1use std::cell::RefCell;
2use std::collections::{HashMap, HashSet};
3use std::rc::Rc;
4
5use repose_core::dnd;
6use repose_core::input::{
7 ImeEvent, Key, KeyEvent, KeyEventType, Modifiers, PointerButton, PointerEvent,
8 PointerEventKind, PointerId, PointerKind,
9};
10use repose_core::locals::{Density, dp_to_px, set_density_default, with_density};
11use repose_core::runtime::{Frame, Scheduler};
12use repose_core::shortcuts::DragAction;
13use repose_core::{
14 CursorIcon, HitRegion, Interaction, RenderContext, Scene, Vec2, View, request_frame,
15 take_focus_request,
16};
17use repose_ui::textfield::{
18 TF_FONT_DP, 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
38#[derive(Clone, Default)]
40pub struct PlatformOutput {
41 pub cursor: Option<CursorIcon>,
43 pub ime_allowed: bool,
45 pub ime_cursor_area: Option<(f64, f64, f64, f64)>,
47 pub clipboard_text: Option<String>,
49
50 pub ime_purpose: repose_core::ImePurposeHint,
53 pub ime_auto_correct: bool,
54 pub ime_capitalization: repose_core::KeyboardCapitalization,
55 pub keyboard_type: repose_core::KeyboardType,
56
57 pub window_theme_dark: Option<bool>,
60}
61
62pub struct FrameOutput {
64 pub scene: Scene,
66 pub hit_regions: Vec<HitRegion>,
68 pub semantics_nodes: Vec<repose_core::runtime::SemNode>,
70 pub focus_chain: Vec<u64>,
72 pub platform: PlatformOutput,
74 pub wants_pointer: bool,
76 pub wants_keyboard: bool,
78}
79
80impl FrameOutput {
81 pub fn into_frame(self) -> Frame {
84 Frame {
85 scene: self.scene,
86 hit_regions: self.hit_regions,
87 semantics_nodes: self.semantics_nodes,
88 focus_chain: self.focus_chain,
89 }
90 }
91}
92
93pub struct PointerMoveResult {
95 pub cursor: Option<CursorIcon>,
97 pub hover_id: Option<u64>,
99}
100
101#[derive(Debug)]
103pub struct PointerButtonResult {
104 pub focused: Option<u64>,
106 pub capture_id: Option<u64>,
108 pub consumed: bool,
110 pub needs_a11y_announce: bool,
112 pub clicked_id: Option<u64>,
115}
116
117const LONG_PRESS_MS: u128 = 500;
119const DOUBLE_CLICK_MS: u128 = 300;
120const DOUBLE_TAP_MIN_MS: u128 = 40;
121const LONG_PRESS_SLOP_DP: f32 = 18.0;
122
123pub struct ReposeRuntime {
129 pub sched: Scheduler,
130 pub scale: f32,
131
132 pub modifiers: Modifiers,
134 pub mouse_pos_px: (f32, f32),
135 pub pointer_inside: bool,
137 pub hover_id: Option<u64>,
138 pub hover_ancestors: std::collections::HashSet<u64>,
139 hover_leave: HashMap<u64, (f32, f32, f32, f32, Rc<dyn Fn(PointerEvent)>)>,
143 pub capture_id: Option<u64>,
144 pub hit_path: Option<Vec<u64>>,
147 pub scroll_capture_id: Option<u64>,
149 last_scroll_at: Option<web_time::Instant>,
150 pub pressed_ids: HashSet<u64>,
151 pub ime_preedit: bool,
152 pub key_pressed_active: Option<u64>,
153 pub last_focus: Option<u64>,
154
155 last_up: Option<(u64, web_time::Instant, f32, f32)>,
156 last_down: Option<(u64, web_time::Instant)>,
160 double_candidate: Option<u64>,
165 long_press: Option<(u64, web_time::Instant, f32, f32)>,
166 key_long_press: Option<(u64, web_time::Instant, bool)>,
169 suppress_next_click: bool,
170 pending_click: Option<(u64, web_time::Instant, Rc<dyn Fn()>)>,
171
172 pub frame_cache: Option<Frame>,
174
175 cursor: Option<CursorIcon>,
177
178 pub textfield_states: HashMap<u64, Rc<RefCell<TextFieldState>>>,
180}
181
182impl ReposeRuntime {
183 pub fn new() -> Self {
184 Self {
185 sched: Scheduler::new(),
186 scale: 1.0,
187 modifiers: Modifiers::default(),
188 mouse_pos_px: (0.0, 0.0),
189 pointer_inside: false,
190 hover_id: None,
191 hover_ancestors: std::collections::HashSet::new(),
192 hover_leave: HashMap::new(),
193 capture_id: None,
194 hit_path: None,
195 scroll_capture_id: None,
196 last_scroll_at: None,
197 pressed_ids: HashSet::new(),
198 ime_preedit: false,
199 key_pressed_active: None,
200 last_focus: None,
201 last_up: None,
202 last_down: None,
203 double_candidate: None,
204 long_press: None,
205 key_long_press: None,
206 suppress_next_click: false,
207 pending_click: None,
208 frame_cache: None,
209 cursor: None,
210 textfield_states: HashMap::new(),
211 }
212 }
213
214 pub fn set_viewport(&mut self, width_px: u32, height_px: u32) {
216 self.sched.size = (width_px, height_px);
217 }
218
219 pub fn set_viewport_and_scale(&mut self, width_px: u32, height_px: u32, scale: f32) {
221 self.scale = scale;
222 self.sched.size = (width_px, height_px);
223 }
224
225 pub fn tick_animations(&self) {
227 repose_core::animation_driver::tick();
228 }
229
230 pub fn poll_gesture_timers(&mut self) {
231 self.poll_long_press();
232 self.flush_pending_click();
233 self.poll_key_long_press();
234 }
235
236 pub fn compose<F>(&mut self, root_fn: &mut F, render_ctx: &RenderContext) -> Frame
241 where
242 F: FnMut(&mut Scheduler, &RenderContext) -> View,
243 {
244 self.poll_long_press();
245 self.flush_pending_click();
246 self.poll_key_long_press();
247
248 let size = self.sched.size;
249 let rc = render_ctx.clone();
250 let mut compose_once = |this: &mut Self| {
253 let mut inner = |s: &mut Scheduler| (root_fn)(s, &rc);
254 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
255 compose_frame_inner_with_ancestors(
256 &mut this.sched,
257 &mut inner,
258 this.scale,
259 size,
260 this.hover_id,
261 &this.hover_ancestors,
262 &this.pressed_ids,
263 &this.textfield_states,
264 )
265 })) {
266 Ok(frame) => frame,
267 Err(_) => {
268 log::error!("compose panicked; presenting last good frame");
269 this.frame_cache.clone().unwrap_or_else(|| Frame {
270 scene: Default::default(),
271 hit_regions: Vec::new(),
272 semantics_nodes: Vec::new(),
273 focus_chain: Vec::new(),
274 })
275 }
276 }
277 };
278
279 let frame = compose_once(self);
280
281 let hover_before = self.hover_id;
285 self.reconcile_hover_from_mouse_pos(&frame);
286 if self.hover_id != hover_before {
287 self.hover_leave.clear();
290 for h in &frame.hit_regions {
291 if let Some(cb) = &h.on_pointer_leave {
292 self.hover_leave
293 .insert(h.id, (h.rect.x, h.rect.y, h.rect.w, h.rect.h, cb.clone()));
294 }
295 }
296 return compose_once(self);
298 }
299 frame
300 }
301
302 pub fn frame(
304 &mut self,
305 mut root_fn: impl FnMut(&mut Scheduler, &RenderContext) -> View,
306 render_ctx: &RenderContext,
307 ) -> FrameOutput {
308 let captured = Rc::new(RefCell::new(None::<String>));
309 let hook = captured.clone();
310 repose_core::clipboard::set_clipboard_observer(Box::new(move |text| {
311 *hook.borrow_mut() = Some(text.to_string());
312 }));
313
314 let f = self.compose(&mut root_fn, render_ctx);
315
316 repose_core::clipboard::clear_clipboard_observer();
317 let clipboard_text = captured.borrow_mut().take();
318
319 let wants_pointer = self.hover_id.is_some() || self.capture_id.is_some();
320
321 let ime_allowed = self.sched.focused.is_some_and(|fid| {
322 f.semantics_nodes
323 .iter()
324 .any(|n| n.id == fid && n.role == repose_core::semantics::Role::TextField)
325 });
326
327 let focused_hit = self
328 .sched
329 .focused
330 .and_then(|fid| f.hit_regions.iter().find(|h| h.id == fid));
331
332 let ime_cursor_area = if ime_allowed {
333 focused_hit.map(|hit| {
334 let sf = self.scale as f64;
335 (
336 hit.rect.x as f64 / sf,
337 hit.rect.y as f64 / sf,
338 hit.rect.w as f64 / sf,
339 hit.rect.h as f64 / sf,
340 )
341 })
342 } else {
343 None
344 };
345
346 let focused_is_textfield = ime_allowed
347 || self.sched.focused.is_some_and(|fid| {
348 f.hit_regions
349 .iter()
350 .any(|h| h.id == fid && h.tf_state_key.is_some())
351 });
352 let wants_keyboard = focused_is_textfield || self.ime_preedit;
353
354 let (ime_purpose, ime_auto_correct, ime_capitalization, keyboard_type) =
355 match (ime_allowed, focused_hit) {
356 (true, Some(hit)) => (
357 hit.keyboard_type.ime_purpose_hint(),
358 hit.auto_correct.unwrap_or(true),
359 hit.capitalization,
360 hit.keyboard_type,
361 ),
362 _ => (
363 repose_core::ImePurposeHint::Normal,
364 true,
365 repose_core::KeyboardCapitalization::Unspecified,
366 repose_core::KeyboardType::Unspecified,
367 ),
368 };
369
370 let platform = PlatformOutput {
371 cursor: self.take_cursor_suggestion(),
372 ime_allowed,
373 ime_cursor_area,
374 clipboard_text,
375 ime_purpose,
376 ime_auto_correct,
377 ime_capitalization,
378 keyboard_type,
379 window_theme_dark: Some(repose_core::locals::theme().is_dark()),
380 };
381 FrameOutput {
382 scene: f.scene,
383 hit_regions: f.hit_regions,
384 semantics_nodes: f.semantics_nodes,
385 focus_chain: f.focus_chain,
386 platform,
387 wants_pointer,
388 wants_keyboard,
389 }
390 }
391
392 pub fn cache_frame(&mut self, frame: Frame) {
394 self.hover_leave.clear();
395 for h in &frame.hit_regions {
396 if let Some(cb) = &h.on_pointer_leave {
397 self.hover_leave
398 .insert(h.id, (h.rect.x, h.rect.y, h.rect.w, h.rect.h, cb.clone()));
399 }
400 }
401 self.frame_cache = Some(frame);
402 }
403
404 pub fn after_compose(&mut self, frame: &Frame, scale: f32) {
409 self.ensure_focused_state_in_frame(frame);
410 self.reconcile_hover_from_mouse_pos(frame);
411 repose_core::dnd::set_dnd_frame(Some(frame.clone()));
412 repose_core::dnd::set_dnd_scale(scale);
413 }
414
415 pub fn ensure_focused_textfield_state(&mut self) {
418 let Some(f) = self.frame_cache.clone() else {
419 return;
420 };
421 self.ensure_focused_state_in_frame(&f);
422 }
423
424 fn ensure_focused_state_in_frame(&mut self, frame: &Frame) {
428 let Some(fid) = self.sched.focused else {
429 return;
430 };
431 if let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
432 && let Some(key) = hit.tf_state_key
433 {
434 let st = ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
435 let mut st = st.borrow_mut();
436 st.apply_controlled_value(&hit.tf_value);
437 st.reset_caret_blink();
438 }
439 }
440
441 pub fn cache_from_output(&mut self, out: &FrameOutput) {
445 let frame = Frame {
446 scene: out.scene.clone(),
447 hit_regions: out.hit_regions.clone(),
448 semantics_nodes: out.semantics_nodes.clone(),
449 focus_chain: out.focus_chain.clone(),
450 };
451 self.after_compose(&frame, self.scale);
452 self.cache_frame(frame);
453 }
454
455 pub fn compose_frame_output<F>(
459 &mut self,
460 root: &mut F,
461 render_ctx: &RenderContext,
462 ) -> FrameOutput
463 where
464 F: FnMut(&mut Scheduler, &RenderContext) -> View,
465 {
466 self.tick_animations();
467 let out = self.frame(root, render_ctx);
468 self.cache_from_output(&out);
469 out
470 }
471
472 fn dispatch_pointer_to_path(&self, kind: PointerEventKind, pos: Vec2, path: &[u64]) {
473 let Some(f) = &self.frame_cache else {
474 return;
475 };
476 let base = PointerEvent::new(
477 PointerId(0),
478 PointerKind::Mouse,
479 kind,
480 pos,
481 1.0,
482 self.modifiers,
483 );
484 for &id in path {
485 let Some(h) = f.hit_regions.iter().find(|h| h.id == id) else {
486 continue;
487 };
488 let cb = match kind {
489 PointerEventKind::Down(_) => &h.on_pointer_down,
490 PointerEventKind::Up(_) => &h.on_pointer_up,
491 PointerEventKind::Move => &h.on_pointer_move,
492 PointerEventKind::Cancel => &h.on_pointer_cancel,
493 PointerEventKind::Enter | PointerEventKind::Leave => continue,
494 };
495 let Some(cb) = cb else {
496 continue;
497 };
498 let mut ev = base.clone();
499 ev.origin = Vec2 {
500 x: h.rect.x,
501 y: h.rect.y,
502 };
503 ev.position = pos - ev.origin;
504 cb(ev);
505 if base.is_consumed() {
506 break;
507 }
508 }
509 }
510
511 pub fn handle_pointer_move(&mut self, pos: Vec2) -> PointerMoveResult {
513 self.mouse_pos_px = (pos.x, pos.y);
514
515 if dnd::handle_drag_action(&DragAction::Move {
517 position: pos,
518 modifiers: self.modifiers,
519 }) {
520 request_frame();
521 return PointerMoveResult {
522 cursor: if dnd::is_dragging() {
523 Some(CursorIcon::Grabbing)
524 } else {
525 self.cursor
526 },
527 hover_id: self.hover_id,
528 };
529 }
530
531 let Some(f) = &self.frame_cache else {
532 return PointerMoveResult {
533 cursor: None,
534 hover_id: None,
535 };
536 };
537
538 if let Some((_, _, x0, y0)) = self.long_press {
539 let slop = LONG_PRESS_SLOP_DP * self.scale;
540 let dx = pos.x - x0;
541 let dy = pos.y - y0;
542 if dx * dx + dy * dy > slop * slop {
543 self.long_press = None;
544 }
545 }
546
547 if self.long_press.is_some()
549 && let Some(lid) = self.long_press.map(|(id, _, _, _)| id)
550 && f.hit_regions
551 .iter()
552 .find(|h| h.id == lid)
553 .map_or(true, |h| !h.rect.contains(pos))
554 {
555 self.long_press = None;
556 }
557
558 if let Some(cid) = self.capture_id
560 && is_textfield_in_frame(f, cid)
561 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
562 {
563 let key = tf_key_of(f, cid);
564 if let Some(st_rc) = self.textfield_states.get(&key) {
565 let mut st = st_rc.borrow_mut();
566 let (ox, oy) = hit.tf_content_origin.unwrap_or((hit.rect.x, hit.rect.y));
567 let content_x = (pos.x - ox + st.scroll_offset).max(0.0);
568 let content_y = (pos.y - oy + st.scroll_offset_y).max(0.0);
569 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
570 let wrap_w = st.inner_width.max(1.0);
571 let idx = if hit.tf_multiline {
572 index_for_xy_bytes_vt(&st, font_px, wrap_w, content_x, content_y)
573 } else {
574 index_for_x_bytes_vt(&st, font_px, content_x)
575 };
576 st.drag_to(idx);
577 }
578 }
579
580 let top = f
582 .hit_regions
583 .iter()
584 .rev()
585 .find(|h| !h.disabled && h.rect.contains(pos));
586
587 self.cursor = top.and_then(|h| h.cursor).or(Some(CursorIcon::Default));
589
590 let new_hover = top.map(|h| h.id);
591
592 let old_chain = hover_chain_for(Some(f), self.hover_id);
594 let new_chain = hover_chain_for(Some(f), new_hover);
595 if new_chain != old_chain {
596 dispatch_hover_change_bubbled(
597 Some(f),
598 &self.hover_leave,
599 &mut self.hover_id,
600 &mut self.hover_ancestors,
601 new_hover,
602 pos,
603 self.modifiers,
604 );
605 request_frame();
606 }
607
608 if let Some(path) = &self.hit_path {
609 self.dispatch_pointer_to_path(PointerEventKind::Move, pos, path);
610 } else if let Some(h) = top
611 && let Some(cb) = &h.on_pointer_move
612 {
613 let mut pe = PointerEvent::new(
614 PointerId(0),
615 PointerKind::Mouse,
616 PointerEventKind::Move,
617 pos,
618 1.0,
619 self.modifiers,
620 );
621 pe.origin = Vec2 {
622 x: h.rect.x,
623 y: h.rect.y,
624 };
625 pe.position = pe.position - pe.origin;
626 cb(pe);
627 }
628
629 PointerMoveResult {
630 cursor: self.cursor,
631 hover_id: self.hover_id,
632 }
633 }
634
635 pub fn handle_pointer_press(
637 &mut self,
638 pos: Vec2,
639 button: PointerButton,
640 ) -> PointerButtonResult {
641 self.mouse_pos_px = (pos.x, pos.y);
642 let _ = repose_core::request_input_mode(repose_core::InputMode::Touch);
643
644 let Some(f) = &self.frame_cache else {
645 return PointerButtonResult {
646 focused: None,
647 capture_id: None,
648 consumed: false,
649 needs_a11y_announce: false,
650 clicked_id: None,
651 };
652 };
653
654 let mut result = PointerButtonResult {
655 focused: None,
656 capture_id: None,
657 consumed: false,
658 needs_a11y_announce: false,
659 clicked_id: None,
660 };
661
662 if let Some(hit) = f
663 .hit_regions
664 .iter()
665 .rev()
666 .find(|h| !h.disabled && h.rect.contains(pos))
667 {
668 let mut path: Vec<u64> = vec![hit.id];
669 let mut cur = hit.parent;
670 while let Some(pid) = cur {
671 path.push(pid);
672 cur = f
673 .hit_regions
674 .iter()
675 .find(|h| h.id == pid)
676 .and_then(|h| h.parent);
677 }
678 self.hit_path = Some(path.clone());
679
680 dnd::handle_drag_action(&DragAction::Press {
682 position: pos,
683 capture_id: hit.id,
684 kind: PointerKind::Mouse,
685 modifiers: self.modifiers,
686 });
687
688 self.capture_id = Some(hit.id);
690 result.capture_id = Some(hit.id);
691 result.consumed = true;
692
693 self.last_down = Some((hit.id, web_time::Instant::now()));
697 self.double_candidate = if hit.on_double_click.is_some()
701 && self.last_up.is_some_and(|(pid, t0, _, _)| {
702 pid == hit.id
703 && self.last_down.is_some_and(|(did, dt)| {
704 did == hit.id
705 && dt.duration_since(t0).as_millis() >= DOUBLE_TAP_MIN_MS
706 && dt.duration_since(t0).as_millis() <= DOUBLE_CLICK_MS
707 })
708 }) {
709 Some(hit.id)
710 } else {
711 None
712 };
713 if self.double_candidate.is_some() {
714 self.pending_click = None;
715 }
716
717 match button {
719 PointerButton::Primary => {
720 self.long_press = if hit.on_long_click.is_some() {
721 Some((hit.id, web_time::Instant::now(), pos.x, pos.y))
722 } else {
723 None
724 };
725 self.suppress_next_click = false;
726 }
727 _ => {}
728 }
729
730 if is_textfield_in_frame(f, hit.id) {
732 let key = tf_key_of(f, hit.id);
733 let seed = hit.tf_value.as_str();
734 let st_rc = ensure_tf_state(&mut self.textfield_states, key, seed);
735 {
736 let mut st = st_rc.borrow_mut();
737 st.apply_controlled_value(seed);
739 st.reset_caret_blink();
740
741 let (ox, oy) = hit.tf_content_origin.unwrap_or((hit.rect.x, hit.rect.y));
742 let content_x = (pos.x - ox + st.scroll_offset).max(0.0);
743 let content_y = (pos.y - oy + st.scroll_offset_y).max(0.0);
744 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
745 let wrap_w = st.inner_width.max(1.0);
746
747 let idx = if hit.tf_multiline {
748 index_for_xy_bytes_vt(&st, font_px, wrap_w, content_x, content_y)
749 } else {
750 index_for_x_bytes_vt(&st, font_px, content_x)
751 };
752 st.handle_pointer_down(idx, (pos.x, pos.y), self.modifiers.shift);
753 }
754 }
755
756 self.pressed_ids.insert(hit.id);
758
759 if hit.focusable {
761 self.sched.focused = Some(hit.id);
762 result.focused = Some(hit.id);
763 if hit.tf_state_key.is_some() {
764 let key = tf_key_of(f, hit.id);
765 let st =
766 ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
767 st.borrow_mut().apply_controlled_value(&hit.tf_value);
768 st.borrow_mut().reset_caret_blink();
769 }
770 }
771
772 self.dispatch_pointer_to_path(PointerEventKind::Down(button), pos, &path);
773
774 request_frame();
775 } else {
776 self.hit_path = None;
778 if self.ime_preedit {
779 self.ime_preedit = false;
780 }
781 self.sched.focused = None;
782 request_frame();
783 }
784
785 result
786 }
787
788 pub fn handle_pointer_release(
790 &mut self,
791 pos: Vec2,
792 button: PointerButton,
793 ) -> PointerButtonResult {
794 self.mouse_pos_px = (pos.x, pos.y);
795 let mut result = PointerButtonResult {
796 focused: self.sched.focused,
797 capture_id: self.capture_id,
798 consumed: false,
799 needs_a11y_announce: false,
800 clicked_id: None,
801 };
802
803 if dnd::handle_drag_action(&DragAction::Release {
804 position: pos,
805 modifiers: self.modifiers,
806 }) {
807 self.capture_id = None;
808 self.hit_path = None;
809 self.pressed_ids.clear();
810 request_frame();
811 result.consumed = true;
812 return result;
813 }
814
815 self.pressed_ids.clear();
816
817 let Some(f) = &self.frame_cache else {
818 self.capture_id = None;
819 self.hit_path = None;
820 return result;
821 };
822
823 if let Some(path) = &self.hit_path {
824 self.dispatch_pointer_to_path(PointerEventKind::Up(button), pos, path);
825 result.consumed = true;
826 }
827
828 if let Some((lid, t0, _, _)) = self.long_press.take() {
830 if Some(lid) == self.capture_id
831 && t0.elapsed().as_millis() >= LONG_PRESS_MS
832 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == lid && !h.disabled)
833 && let Some(cb) = &hit.on_long_click
834 {
835 cb();
836 self.suppress_next_click = true;
837 self.pending_click = None;
838 result.clicked_id = Some(lid);
839 result.needs_a11y_announce = true;
840 result.consumed = true;
841 }
842 }
843
844 if self.double_candidate.is_none()
845 && !self.suppress_next_click
846 && let Some(cid) = self.capture_id
847 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid && !h.disabled)
848 && hit.rect.contains(pos)
849 {
850 let now = web_time::Instant::now();
851 if hit.on_double_click.is_some() {
854 if let Some(cb) = hit.on_click.clone() {
855 self.pending_click = Some((cid, now, cb));
856 }
857 self.last_up = Some((cid, now, pos.x, pos.y));
858 result.consumed = true;
859 request_frame(); } else {
861 if let Some(cb) = &hit.on_click {
862 cb();
863 }
864 self.last_up = Some((cid, now, pos.x, pos.y));
865 result.clicked_id = Some(cid);
866 result.needs_a11y_announce = true;
867 result.consumed = true;
868 }
869 }
870 self.suppress_next_click = false;
871
872 if let Some(dc) = self.double_candidate.take() {
876 self.pending_click = None;
877 self.last_up = None;
878 self.last_down = None;
879 if self.capture_id == Some(dc)
880 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == dc && !h.disabled)
881 && hit.rect.contains(pos)
882 {
883 if let Some(cb) = &hit.on_double_click {
884 cb();
885 }
886 result.clicked_id = Some(dc);
887 result.needs_a11y_announce = true;
888 result.consumed = true;
889 } else if self.capture_id == Some(dc)
890 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == dc && !h.disabled)
891 {
892 if let Some(cb) = &hit.on_click {
894 cb();
895 }
896 result.clicked_id = Some(dc);
897 result.needs_a11y_announce = true;
898 result.consumed = true;
899 }
900 }
901
902 if let Some(cid) = self.capture_id
904 && is_textfield_in_frame(f, cid)
905 {
906 let key = tf_key_of(f, cid);
907 if let Some(state_rc) = self.textfield_states.get(&key) {
908 state_rc.borrow_mut().end_drag();
909 }
910 }
911
912 self.capture_id = None;
913 self.hit_path = None;
914 request_frame();
915 result
916 }
917
918 pub fn handle_pointer_cancel(&mut self) {
920 self.long_press = None;
921 self.last_up = None;
922 dnd::handle_drag_action(&DragAction::Cancel);
923 let pos = Vec2 {
924 x: self.mouse_pos_px.0,
925 y: self.mouse_pos_px.1,
926 };
927 dispatch_hover_change_bubbled(
928 self.frame_cache.as_ref(),
929 &self.hover_leave,
930 &mut self.hover_id,
931 &mut self.hover_ancestors,
932 None,
933 pos,
934 self.modifiers,
935 );
936 if let Some(path) = &self.hit_path {
937 self.dispatch_pointer_to_path(PointerEventKind::Cancel, pos, path);
938 }
939 self.reset_pointer_state();
940 }
941
942 pub fn clear_hover(&mut self) {
944 if self.hover_id.is_none() && self.hover_ancestors.is_empty() {
945 return;
946 }
947 let pos = Vec2 {
948 x: self.mouse_pos_px.0,
949 y: self.mouse_pos_px.1,
950 };
951 dispatch_hover_change_bubbled(
952 self.frame_cache.as_ref(),
953 &self.hover_leave,
954 &mut self.hover_id,
955 &mut self.hover_ancestors,
956 None,
957 pos,
958 self.modifiers,
959 );
960 }
961
962 pub fn reconcile_hover_from_mouse_pos(&mut self, new_frame: &Frame) {
964 let pos = Vec2 {
965 x: self.mouse_pos_px.0,
966 y: self.mouse_pos_px.1,
967 };
968
969 if let Some(prev_id) = self.hover_id
972 && !new_frame.hit_regions.iter().any(|h| h.id == prev_id)
973 {
974 dispatch_hover_change_bubbled(
975 Some(new_frame),
976 &self.hover_leave,
977 &mut self.hover_id,
978 &mut self.hover_ancestors,
979 None,
980 pos,
981 self.modifiers,
982 );
983 }
984
985 if !self.pointer_inside {
986 if self.hover_id.is_some() || !self.hover_ancestors.is_empty() {
987 dispatch_hover_change_bubbled(
988 Some(new_frame),
989 &self.hover_leave,
990 &mut self.hover_id,
991 &mut self.hover_ancestors,
992 None,
993 pos,
994 self.modifiers,
995 );
996 request_frame();
997 }
998 return;
999 }
1000
1001 let new_hover = new_frame
1002 .hit_regions
1003 .iter()
1004 .rev()
1005 .find(|h| !h.disabled && h.rect.contains(pos))
1006 .map(|h| h.id);
1007
1008 self.cursor = if dnd::is_dragging() {
1009 Some(CursorIcon::Grabbing)
1010 } else {
1011 new_hover
1012 .and_then(|id| new_frame.hit_regions.iter().find(|h| h.id == id))
1013 .and_then(|h| h.cursor)
1014 .or(Some(CursorIcon::Default))
1015 };
1016
1017 let new_chain = hover_chain_for(Some(new_frame), new_hover);
1018 let old_chain = hover_chain_for(Some(new_frame), self.hover_id);
1019 if new_chain == old_chain {
1020 return;
1021 }
1022
1023 dispatch_hover_change_bubbled(
1024 Some(new_frame),
1025 &self.hover_leave,
1026 &mut self.hover_id,
1027 &mut self.hover_ancestors,
1028 new_hover,
1029 pos,
1030 self.modifiers,
1031 );
1032 request_frame();
1033 }
1034
1035 fn reset_pointer_state(&mut self) {
1036 self.capture_id = None;
1037 self.hit_path = None;
1038 self.pressed_ids.clear();
1039 self.pending_click = None;
1040 self.last_down = None;
1041 self.double_candidate = None;
1042 self.key_long_press = None;
1043 self.suppress_next_click = false;
1044 }
1045
1046 fn flush_pending_click(&mut self) {
1047 let Some((id, t0, cb)) = self.pending_click.take() else {
1048 return;
1049 };
1050 if t0.elapsed().as_millis() >= DOUBLE_CLICK_MS {
1051 cb();
1052 request_frame();
1053 } else {
1054 self.pending_click = Some((id, t0, cb));
1055 request_frame();
1056 }
1057 }
1058
1059 fn poll_long_press(&mut self) {
1060 let Some(f) = self.frame_cache.clone() else {
1061 return;
1062 };
1063 let Some((lid, t0, _, _)) = self.long_press else {
1064 return;
1065 };
1066 if t0.elapsed().as_millis() < LONG_PRESS_MS {
1067 request_frame();
1068 return;
1069 }
1070 if self.capture_id != Some(lid) {
1073 self.long_press = None;
1074 return;
1075 }
1076 let (mx, my) = self.mouse_pos_px;
1077 let in_bounds = f
1078 .hit_regions
1079 .iter()
1080 .find(|h| h.id == lid)
1081 .map_or(false, |h| h.rect.contains(Vec2 { x: mx, y: my }));
1082 if !in_bounds {
1083 self.long_press = None;
1084 return;
1085 }
1086 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == lid && !h.disabled)
1087 && let Some(cb) = &hit.on_long_click
1088 {
1089 self.long_press = None;
1090 self.suppress_next_click = true;
1091 self.pending_click = None;
1092 self.last_up = None;
1093 cb();
1094 request_frame();
1095 } else {
1096 self.long_press = None;
1097 }
1098 }
1099
1100 fn poll_key_long_press(&mut self) {
1104 let Some(f) = self.frame_cache.clone() else {
1105 return;
1106 };
1107 let Some((kid, t0, fired)) = self.key_long_press else {
1108 return;
1109 };
1110 if t0.elapsed().as_millis() < LONG_PRESS_MS {
1111 request_frame();
1112 return;
1113 }
1114 if !fired {
1115 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == kid && !h.disabled)
1116 && let Some(cb) = &hit.on_long_click
1117 {
1118 cb();
1119 }
1120 self.key_long_press = Some((kid, t0, true));
1121 request_frame();
1122 }
1123 }
1124
1125 pub fn handle_scroll(&mut self, delta: Vec2) -> bool {
1127 let Some(f) = &self.frame_cache else {
1128 return false;
1129 };
1130
1131 let now = web_time::Instant::now();
1132 if let Some(last) = self.last_scroll_at
1133 && now.duration_since(last).as_millis() > 250
1134 {
1135 self.scroll_capture_id = None;
1136 }
1137 self.last_scroll_at = Some(now);
1138
1139 let pos = Vec2 {
1140 x: self.mouse_pos_px.0,
1141 y: self.mouse_pos_px.1,
1142 };
1143 let (consumed, cap) = dispatch_scroll(f, pos, delta, self.scroll_capture_id);
1144 self.scroll_capture_id = cap;
1145 if consumed {
1146 request_frame();
1147 }
1148 consumed
1149 }
1150
1151 pub fn handle_key(&mut self, event: &KeyEvent) -> bool {
1153 if event.event_type == KeyEventType::Down {
1155 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
1156 }
1157
1158 let Some(frame) = self.frame_cache.clone() else {
1161 return false;
1162 };
1163 let f = &frame;
1164
1165 if event.event_type == KeyEventType::Down && !event.is_repeat && event.key == Key::Escape {
1169 if dnd::handle_drag_action(&DragAction::Cancel) {
1170 request_frame();
1171 return true;
1172 }
1173 if self.dispatch_focus_key_event(f, event) {
1175 request_frame();
1176 return true;
1177 }
1178 return false;
1179 }
1180
1181 let consumed = self.dispatch_focus_key_event(f, event);
1183 if consumed {
1184 request_frame();
1185 return true;
1186 }
1187
1188 if event.event_type == KeyEventType::Down
1190 && !event.is_repeat
1191 && let Some(action) = repose_core::shortcuts::resolve_action(
1192 repose_core::shortcuts::KeyChord::new(event.key.clone(), self.modifiers),
1193 )
1194 {
1195 if self.dispatch_action(action.clone()) {
1197 return true;
1198 }
1199 }
1200
1201 if let Some(fid) = self.sched.focused {
1203 let is_tf = f
1204 .semantics_nodes
1205 .iter()
1206 .any(|n| n.id == fid && n.role == repose_core::semantics::Role::TextField);
1207 if !is_tf {
1208 if event.event_type == KeyEventType::Down && !event.is_repeat {
1209 if event.key == Key::Space || event.key == Key::Enter {
1210 let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) else {
1211 return false;
1212 };
1213 if hit.on_click.is_none()
1214 && hit.on_long_click.is_none()
1215 && hit.on_double_click.is_none()
1216 {
1217 return false; }
1219 self.pressed_ids.insert(fid);
1220 self.key_pressed_active = Some(fid);
1221 self.key_long_press = if hit.on_long_click.is_some() {
1222 Some((fid, web_time::Instant::now(), false))
1223 } else {
1224 None
1225 };
1226
1227 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1228 if let Some(src) = &hit.interaction_source {
1229 let local = Vec2 {
1230 x: hit.rect.w * 0.5,
1231 y: hit.rect.h * 0.5,
1232 };
1233 src.to_mutable().emit(Interaction::new_press(local));
1234 }
1235 }
1236
1237 request_frame();
1238 return true;
1239 }
1240 } else if event.event_type == KeyEventType::Up
1241 && let Some(active_id) = self.key_pressed_active
1242 && (event.key == Key::Space || event.key == Key::Enter)
1243 {
1244 self.pressed_ids.remove(&active_id);
1245 self.key_pressed_active = None;
1246
1247 let long_fired = self
1248 .key_long_press
1249 .take()
1250 .map(|(_, _, fired)| fired)
1251 .unwrap_or(false);
1252
1253 if let Some(hit) = f
1254 .hit_regions
1255 .iter()
1256 .find(|h| h.id == active_id && !h.disabled)
1257 {
1258 if let Some(src) = &hit.interaction_source {
1259 let pid = src.collect_last_press_id().unwrap_or(0);
1260 src.to_mutable().emit(Interaction::Release(pid));
1261 }
1262 if !long_fired {
1263 if let Some(cb) = &hit.on_click {
1264 cb();
1265 }
1266 }
1267 }
1268 request_frame();
1269 return true;
1270 }
1271 }
1272 }
1273
1274 if event.event_type == KeyEventType::Down
1276 && !event.is_repeat
1277 && event.key == Key::Enter
1278 && let Some(fid) = self.sched.focused
1279 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1280 {
1281 let is_multiline = hit.tf_multiline;
1282 let should_submit = if is_multiline {
1283 self.modifiers.ctrl || self.modifiers.meta
1284 } else {
1285 true
1286 };
1287 if should_submit {
1288 if let Some(on_submit) = &hit.on_text_submit {
1289 let key = tf_key_of(f, fid);
1290 if let Some(state_rc) = self.textfield_states.get(&key) {
1291 let text = state_rc.borrow().text.clone();
1292 on_submit(text);
1293 request_frame();
1294 return true;
1295 }
1296 }
1297 } else {
1298 let key = tf_key_of(f, fid);
1300 if !is_tf_editable(f, fid) {
1301 return true;
1302 }
1303 if let Some(state_rc) = self.textfield_states.get(&key) {
1304 let mut st = state_rc.borrow_mut();
1305 st.insert_text("\n");
1306 let new_text = st.text.clone();
1307 notify_text_change(f, fid, new_text);
1308 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1309 request_frame();
1310 return true;
1311 }
1312 }
1313 }
1314
1315 if event.event_type == KeyEventType::Down {
1317 if let Some(fid) = self.sched.focused {
1318 let key = tf_key_of(f, fid);
1319 if let Some(state_rc) = self.textfield_states.get(&key) {
1320 let mut state = state_rc.borrow_mut();
1321 match event.key {
1322 Key::Backspace => {
1323 if !is_tf_editable(f, fid) {
1324 return true;
1325 }
1326 state.delete_backward();
1327 let new_text = state.text.clone();
1328 notify_text_change(f, fid, new_text);
1329 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1330 request_frame();
1331 return true;
1332 }
1333 Key::Delete => {
1334 if !is_tf_editable(f, fid) {
1335 return true;
1336 }
1337 state.delete_forward();
1338 let new_text = state.text.clone();
1339 notify_text_change(f, fid, new_text);
1340 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1341 request_frame();
1342 return true;
1343 }
1344 Key::ArrowLeft => {
1345 state.move_cursor(-1, self.modifiers.shift);
1346 state.preferred_x_px = None;
1347 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1348 request_frame();
1349 return true;
1350 }
1351 Key::ArrowRight => {
1352 state.move_cursor(1, self.modifiers.shift);
1353 state.preferred_x_px = None;
1354 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1355 request_frame();
1356 return true;
1357 }
1358 Key::ArrowUp => {
1359 if is_multiline_id(f, fid)
1360 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1361 {
1362 let font_px = dp_to_px(TF_FONT_DP);
1363 let cur = state.caret_index();
1364 let (new_pos, px) = repose_ui::textfield::move_caret_vertical(
1365 &state.text,
1366 font_px,
1367 hit.rect.w,
1368 cur,
1369 -1,
1370 state.preferred_x_px,
1371 );
1372 if self.modifiers.shift {
1373 state.selection.end = new_pos;
1374 } else {
1375 state.selection = new_pos..new_pos;
1376 }
1377 state.preferred_x_px = Some(px);
1378 let (cx, cy, _) = caret_xy_for_byte(
1379 &state.text,
1380 font_px,
1381 hit.rect.w,
1382 state.caret_index(),
1383 );
1384 let iw = state.inner_width;
1385 let ih = state.inner_height;
1386 state.ensure_caret_visible_xy(cx, cy, iw, ih, dp_to_px(2.0));
1387 request_frame();
1388 return true;
1389 }
1390 }
1391 Key::ArrowDown => {
1392 if is_multiline_id(f, fid)
1393 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1394 {
1395 let font_px = dp_to_px(TF_FONT_DP);
1396 let cur = state.caret_index();
1397 let (new_pos, px) = repose_ui::textfield::move_caret_vertical(
1398 &state.text,
1399 font_px,
1400 hit.rect.w,
1401 cur,
1402 1,
1403 state.preferred_x_px,
1404 );
1405 if self.modifiers.shift {
1406 state.selection.end = new_pos;
1407 } else {
1408 state.selection = new_pos..new_pos;
1409 }
1410 state.preferred_x_px = Some(px);
1411 let (cx, cy, _) = caret_xy_for_byte(
1412 &state.text,
1413 font_px,
1414 hit.rect.w,
1415 state.caret_index(),
1416 );
1417 let iw = state.inner_width;
1418 let ih = state.inner_height;
1419 state.ensure_caret_visible_xy(cx, cy, iw, ih, dp_to_px(2.0));
1420 request_frame();
1421 return true;
1422 }
1423 }
1424 Key::Home => {
1425 state.selection = 0..0;
1426 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1427 request_frame();
1428 return true;
1429 }
1430 Key::End => {
1431 let end = state.text.len();
1432 state.selection = end..end;
1433 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
1434 request_frame();
1435 return true;
1436 }
1437 _ => {}
1438 }
1439 }
1440 }
1441
1442 if !self.ime_preedit
1444 && !self.modifiers.ctrl
1445 && !self.modifiers.alt
1446 && !self.modifiers.meta
1447 && let Key::Character(c) = event.key
1448 && !c.is_control()
1449 && c != '\n'
1450 && c != '\r'
1451 && let Some(fid) = self.sched.focused
1452 {
1453 let key = tf_key_of(f, fid);
1454 if !is_tf_editable(f, fid) {
1455 return true;
1456 }
1457 if let Some(state_rc) = self.textfield_states.get(&key) {
1458 let mut st = state_rc.borrow_mut();
1459 let text = c.to_string();
1460 st.insert_text(&text);
1461 notify_text_change(f, fid, st.text.clone());
1462 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1463 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1464 }
1465 request_frame();
1466 return true;
1467 }
1468 }
1469 }
1470
1471 false
1472 }
1473
1474 fn dispatch_focus_key_event(&self, f: &Frame, event: &KeyEvent) -> bool {
1476 let Some(focused) = self.sched.focused else {
1477 return false;
1478 };
1479
1480 let hit_by_id: HashMap<u64, &HitRegion> = f.hit_regions.iter().map(|h| (h.id, h)).collect();
1481 let sem_parent_of: HashMap<u64, u64> = f
1482 .semantics_nodes
1483 .iter()
1484 .filter_map(|n| n.parent.map(|p| (n.id, p)))
1485 .collect();
1486
1487 let mut ancestors = Vec::new();
1489 let mut cur = focused;
1490 loop {
1491 ancestors.push(cur);
1492 if let Some(&p) = sem_parent_of.get(&cur) {
1493 cur = p;
1494 } else {
1495 break;
1496 }
1497 }
1498
1499 for &id in ancestors.iter().rev() {
1501 if let Some(hit) = hit_by_id.get(&id)
1502 && let Some(cb) = &hit.on_preview_key_event
1503 && cb(event.clone())
1504 {
1505 return true;
1506 }
1507 }
1508
1509 for &id in ancestors.iter() {
1511 if let Some(hit) = hit_by_id.get(&id)
1512 && let Some(cb) = &hit.on_key_event
1513 && cb(event.clone())
1514 {
1515 return true;
1516 }
1517 }
1518
1519 false
1520 }
1521
1522 pub fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
1526 if let Some(f) = &self.frame_cache
1528 && let Some(fid) = self.sched.focused
1529 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
1530 && let Some(cb) = &hit.on_action
1531 && cb(action.clone())
1532 {
1533 request_frame();
1534 return true;
1535 }
1536
1537 if self.apply_text_editing_action(&action) {
1539 return true;
1540 }
1541
1542 if repose_core::shortcuts::handle(action.clone()) {
1544 request_frame();
1545 return true;
1546 }
1547
1548 if let Some(f) = self.frame_cache.clone()
1550 && let Some(new_id) = repose_core::focus::handle_action(&action, &mut self.sched, &f)
1551 {
1552 if let Some(active) = self.key_pressed_active.take() {
1554 self.pressed_ids.remove(&active);
1555 }
1556 self.key_long_press = None;
1557 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == new_id)
1559 && let Some(key) = hit.tf_state_key
1560 {
1561 let st = ensure_tf_state(&mut self.textfield_states, key, hit.tf_value.as_str());
1562 {
1563 let mut s = st.borrow_mut();
1564 s.apply_controlled_value(&hit.tf_value);
1565 s.reset_caret_blink();
1566 }
1567 }
1568 request_frame();
1569 return true;
1570 }
1571
1572 false
1573 }
1574
1575 fn apply_text_editing_action(&mut self, action: &repose_core::shortcuts::Action) -> bool {
1578 use repose_core::shortcuts::Action;
1579 let Some(fid) = self.sched.focused else {
1580 return false;
1581 };
1582 let Some(f) = self.frame_cache.clone() else {
1583 return false;
1584 };
1585 if !is_textfield_in_frame(&f, fid) {
1586 return false;
1587 }
1588 let key = tf_key_of(&f, fid);
1589 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
1590 return false;
1591 };
1592 let multiline = is_multiline_id(&f, fid);
1593
1594 match action {
1595 Action::Undo => {
1596 let mut st = state_rc.borrow_mut();
1597 if !st.can_undo() {
1598 return false;
1599 }
1600 st.undo();
1601 notify_text_change(&f, fid, st.text.clone());
1602 tf_ensure_caret_visible(&mut st, multiline);
1603 request_frame();
1604 true
1605 }
1606 Action::Redo => {
1607 let mut st = state_rc.borrow_mut();
1608 if !st.can_redo() {
1609 return false;
1610 }
1611 st.redo();
1612 notify_text_change(&f, fid, st.text.clone());
1613 tf_ensure_caret_visible(&mut st, multiline);
1614 request_frame();
1615 true
1616 }
1617 Action::SelectAll => {
1618 let mut st = state_rc.borrow_mut();
1619 let len = st.text.len();
1620 st.selection = 0..len;
1621 request_frame();
1622 true
1623 }
1624 Action::Copy => {
1625 let st = state_rc.borrow();
1626 let (a, b) = (
1627 st.selection.start.min(st.selection.end),
1628 st.selection.start.max(st.selection.end),
1629 );
1630 if a == b {
1631 return false;
1632 }
1633 let slice = st.text.get(a..b).unwrap_or("").to_string();
1634 drop(st);
1635 if !slice.is_empty() {
1636 repose_core::clipboard::copy_to_clipboard(&slice);
1637 }
1638 true
1639 }
1640 Action::Cut => {
1641 if !is_tf_editable(&f, fid) {
1642 return false;
1643 }
1644 let mut st = state_rc.borrow_mut();
1645 let (a, b) = (
1646 st.selection.start.min(st.selection.end),
1647 st.selection.start.max(st.selection.end),
1648 );
1649 if a == b {
1650 return false;
1651 }
1652 let slice = st.text.get(a..b).unwrap_or("").to_string();
1653 st.insert_text_atomic("");
1655 let new_text = st.text.clone();
1656 drop(st);
1657 if !slice.is_empty() {
1658 repose_core::clipboard::copy_to_clipboard(&slice);
1659 }
1660 notify_text_change(&f, fid, new_text);
1661 if let Some(mut st) = self.textfield_states.get(&key).map(|s| s.borrow_mut()) {
1662 tf_ensure_caret_visible(&mut st, multiline);
1663 }
1664 request_frame();
1665 true
1666 }
1667 Action::Paste => {
1668 if let Some(txt) = repose_core::clipboard::paste_text() {
1669 self.paste_into_focused(&txt);
1670 return true;
1671 }
1672 false
1673 }
1674 _ => false,
1675 }
1676 }
1677
1678 pub fn handle_ime(&mut self, event: &ImeEvent) {
1680 let Some(fid) = self.sched.focused else {
1681 return;
1682 };
1683 let Some(f) = &self.frame_cache else {
1684 return;
1685 };
1686 if !is_tf_editable(f, fid) {
1687 return;
1688 }
1689 let key = tf_key_of(f, fid);
1690 let Some(state_rc) = self.textfield_states.get(&key) else {
1691 return;
1692 };
1693
1694 let mut state = state_rc.borrow_mut();
1695
1696 match event {
1697 ImeEvent::Start => {
1698 self.ime_preedit = false;
1699 }
1700 ImeEvent::Update { text, cursor } => {
1701 state.set_composition(text.clone(), *cursor);
1702 self.ime_preedit = !text.is_empty();
1703 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1704 notify_text_change(f, fid, state.text.clone());
1705 }
1706 ImeEvent::Commit(text) => {
1707 state.commit_composition(text.clone());
1708 self.ime_preedit = false;
1709 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1710 notify_text_change(f, fid, state.text.clone());
1711 }
1712 ImeEvent::Cancel => {
1713 self.ime_preedit = false;
1714 if state.composition.is_some() {
1715 state.cancel_composition();
1716 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1717 notify_text_change(f, fid, state.text.clone());
1718 }
1719 }
1720 }
1721
1722 request_frame();
1723 }
1724
1725 pub fn handle_focus_lost(&mut self) {
1727 dnd::handle_drag_action(&DragAction::Cancel);
1728 self.handle_pointer_cancel();
1729 self.ime_preedit = false;
1730 }
1731
1732 pub fn ensure_textfield_state(&mut self, key: u64) -> Rc<RefCell<TextFieldState>> {
1734 self.textfield_states
1735 .entry(key)
1736 .or_insert_with(|| Rc::new(RefCell::new(TextFieldState::new())))
1737 .clone()
1738 }
1739
1740 pub fn ensure_textfield_state_seeded(
1741 &mut self,
1742 key: u64,
1743 seed: &str,
1744 ) -> Rc<RefCell<TextFieldState>> {
1745 ensure_tf_state(&mut self.textfield_states, key, seed)
1746 }
1747
1748 pub fn tf_key_of(&self, visual_id: u64) -> u64 {
1750 self.frame_cache
1751 .as_ref()
1752 .map(|f| tf_key_of(f, visual_id))
1753 .unwrap_or(visual_id)
1754 }
1755
1756 pub fn is_textfield(&self, id: u64) -> bool {
1758 self.frame_cache
1759 .as_ref()
1760 .map(|f| is_textfield_in_frame(f, id))
1761 .unwrap_or(false)
1762 }
1763
1764 pub fn is_multiline(&self, id: u64) -> bool {
1766 self.frame_cache
1767 .as_ref()
1768 .map(|f| is_multiline_id(f, id))
1769 .unwrap_or(false)
1770 }
1771
1772 pub fn focused_keyboard_hints(
1775 &self,
1776 ) -> (
1777 repose_core::ImePurposeHint,
1778 bool,
1779 repose_core::KeyboardCapitalization,
1780 ) {
1781 let defaults = || {
1782 (
1783 repose_core::ImePurposeHint::Normal,
1784 true,
1785 repose_core::KeyboardCapitalization::Unspecified,
1786 )
1787 };
1788 let Some(fid) = self.sched.focused else {
1789 return defaults();
1790 };
1791 let Some(f) = &self.frame_cache else {
1792 return defaults();
1793 };
1794 match f.hit_regions.iter().find(|h| h.id == fid) {
1795 Some(hit) => (
1796 hit.keyboard_type.ime_purpose_hint(),
1797 hit.auto_correct.unwrap_or(true),
1798 hit.capitalization,
1799 ),
1800 None => defaults(),
1801 }
1802 }
1803
1804 pub fn insert_text_into_focused(&mut self, text: &str) -> bool {
1812 if text.is_empty()
1813 || self.ime_preedit
1814 || self.modifiers.ctrl
1815 || self.modifiers.alt
1816 || self.modifiers.meta
1817 {
1818 return false;
1819 }
1820 let Some(fid) = self.sched.focused else {
1821 return false;
1822 };
1823 let Some(f) = self.frame_cache.clone() else {
1824 return false;
1825 };
1826 if !is_textfield_in_frame(&f, fid) {
1827 return false;
1828 }
1829 if !is_tf_editable(&f, fid) {
1830 return false;
1831 }
1832 let key = tf_key_of(&f, fid);
1833 let Some(state_rc) = self.textfield_states.get(&key).cloned() else {
1834 return false;
1835 };
1836 let multiline = is_multiline_id(&f, fid);
1837 let filtered: String = text
1838 .chars()
1839 .filter(|c| {
1840 (*c == '\n' && multiline) || (!c.is_control() && *c != '\r')
1844 })
1845 .collect();
1846 if filtered.is_empty() {
1847 return false;
1848 }
1849 {
1850 let mut st = state_rc.borrow_mut();
1851 st.insert_text(&filtered);
1852 let new_text = st.text.clone();
1853 notify_text_change(&f, fid, new_text);
1854 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1855 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1856 }
1857 }
1858 request_frame();
1859 true
1860 }
1861
1862 pub fn insert_text(&mut self, text: &str) -> bool {
1866 self.insert_text_into_focused(text)
1867 }
1868
1869 pub fn paste_into_focused(&mut self, text: &str) {
1872 let Some(fid) = self.sched.focused else {
1873 return;
1874 };
1875 let Some(f) = &self.frame_cache.clone() else {
1876 return;
1877 };
1878 if !is_tf_editable(f, fid) {
1879 return;
1880 }
1881 let key = tf_key_of(f, fid);
1882 if let Some(state_rc) = self.textfield_states.get(&key) {
1883 let mut st = state_rc.borrow_mut();
1884 st.insert_text_atomic(text);
1885 let new_text = st.text.clone();
1886 notify_text_change(f, fid, new_text);
1887 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1888 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1889 }
1890 }
1891 request_frame();
1892 }
1893
1894 pub fn handle_key_with_text(&mut self, event: &KeyEvent, composed_text: Option<&str>) -> bool {
1902 if event.event_type == KeyEventType::Down {
1903 let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
1904 }
1905 if event.event_type == KeyEventType::Down
1906 && !event.is_repeat
1907 && !self.ime_preedit
1908 && !self.modifiers.ctrl
1909 && !self.modifiers.alt
1910 && !self.modifiers.meta
1911 && let Some(text) = composed_text
1912 && !text.chars().all(|c| c.is_control())
1913 && self.insert_text_into_focused(text)
1914 {
1915 return true;
1916 }
1917 self.handle_key(event)
1918 }
1919
1920 pub fn handle_scroll_at(
1924 &mut self,
1925 pos: Vec2,
1926 delta: Vec2,
1927 scroll_capture: Option<u64>,
1928 ) -> (bool, Option<u64>) {
1929 let Some(f) = &self.frame_cache else {
1930 return (false, scroll_capture);
1931 };
1932 let (consumed, cap) = dispatch_scroll(f, pos, delta, scroll_capture);
1933 if consumed {
1934 request_frame();
1935 }
1936 (consumed, cap)
1937 }
1938
1939 pub fn next_caret_blink_deadline(&self) -> Option<web_time::Instant> {
1941 let fid = self.sched.focused?;
1942 let frame = self.frame_cache.as_ref()?;
1943 let hit = frame.hit_regions.iter().find(|h| h.id == fid)?;
1944 let key = hit.tf_state_key?;
1945 self.textfield_states
1946 .get(&key)?
1947 .borrow()
1948 .next_blink_deadline()
1949 }
1950
1951 pub fn tick_overlays(&self, last_frame: web_time::Instant) {
1954 let now = web_time::Instant::now();
1955 let ms = now
1956 .saturating_duration_since(last_frame)
1957 .as_millis()
1958 .min(u32::MAX as u128) as u32;
1959 if ms > 0 {
1960 repose_ui::overlay::SnackbarController::tick_for_frame(ms);
1961 }
1962 }
1963
1964 pub fn cursor_suggestion(&self) -> Option<CursorIcon> {
1966 self.cursor
1967 }
1968
1969 pub fn take_cursor_suggestion(&mut self) -> Option<CursorIcon> {
1971 self.cursor.take()
1972 }
1973}
1974
1975impl Default for ReposeRuntime {
1976 fn default() -> Self {
1977 Self::new()
1978 }
1979}
1980
1981pub fn compose_frame_inner<F>(
1983 sched: &mut Scheduler,
1984 root_fn: &mut F,
1985 scale: f32,
1986 size_px_u32: (u32, u32),
1987 hover_id: Option<u64>,
1988 pressed_ids: &HashSet<u64>,
1989 tf_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
1990) -> Frame
1991where
1992 F: FnMut(&mut Scheduler) -> View,
1993{
1994 compose_frame_inner_with_ancestors(
1995 sched,
1996 root_fn,
1997 scale,
1998 size_px_u32,
1999 hover_id,
2000 &std::collections::HashSet::new(),
2001 pressed_ids,
2002 tf_states,
2003 )
2004}
2005
2006pub fn compose_frame_inner_with_ancestors<F>(
2007 sched: &mut Scheduler,
2008 root_fn: &mut F,
2009 scale: f32,
2010 size_px_u32: (u32, u32),
2011 hover_id: Option<u64>,
2012 hover_ancestors: &std::collections::HashSet<u64>,
2013 pressed_ids: &HashSet<u64>,
2014 tf_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
2015) -> Frame
2016where
2017 F: FnMut(&mut Scheduler) -> View,
2018{
2019 if let Some(requested_id) = take_focus_request() {
2020 if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
2021 sched.focused = None;
2022 } else {
2023 sched.focused = Some(requested_id);
2024 }
2025 }
2026
2027 set_density_default(Density { scale });
2028
2029 let current_focused = sched.focused;
2030
2031 let frame = sched.repose(
2032 {
2033 let scale = scale;
2034 move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s))
2035 },
2036 {
2037 let hover_id = hover_id;
2038 let hover_ancestors = hover_ancestors.clone();
2039 let pressed_ids = pressed_ids.clone();
2040 move |view, _size| {
2041 let interactions = Interactions {
2042 hover: hover_id,
2043 hover_ancestors: hover_ancestors.clone(),
2044 pressed: pressed_ids.clone(),
2045 };
2046 with_density(Density { scale }, || {
2047 layout_and_paint(view, size_px_u32, tf_states, &interactions, current_focused)
2048 })
2049 }
2050 },
2051 );
2052
2053 if let Some(fid) = sched.focused
2054 && !frame.focus_chain.contains(&fid)
2055 {
2056 sched.focused = None;
2057 }
2058
2059 frame
2060}
2061
2062fn dispatch_hover_change(
2065 frame: Option<&Frame>,
2066 leave_map: &HashMap<u64, (f32, f32, f32, f32, Rc<dyn Fn(PointerEvent)>)>,
2067 hover_id: &mut Option<u64>,
2068 new_hover: Option<u64>,
2069 pos: Vec2,
2070 modifiers: Modifiers,
2071) {
2072 if new_hover == *hover_id {
2073 return;
2074 }
2075
2076 if let Some(prev_id) = *hover_id {
2078 let leave_info = leave_map.get(&prev_id).cloned().or_else(|| {
2079 frame.and_then(|f| {
2080 f.hit_regions
2081 .iter()
2082 .find(|h| h.id == prev_id)
2083 .and_then(|h| {
2084 h.on_pointer_leave
2085 .as_ref()
2086 .map(|cb| (h.rect.x, h.rect.y, h.rect.w, h.rect.h, cb.clone()))
2087 })
2088 })
2089 });
2090 if let Some((rx, ry, _rw, _rh, cb)) = leave_info {
2091 let mut pe = PointerEvent::new(
2092 PointerId(0),
2093 PointerKind::Mouse,
2094 PointerEventKind::Leave,
2095 pos,
2096 1.0,
2097 modifiers,
2098 );
2099 pe.origin = Vec2 { x: rx, y: ry };
2100 pe.position = pe.position - pe.origin;
2101 cb(pe);
2102 }
2103 }
2104
2105 if let Some(f) = frame
2107 && let Some(hid) = new_hover
2108 && let Some(h) = f.hit_regions.iter().find(|h| h.id == hid)
2109 && let Some(cb) = &h.on_pointer_enter
2110 {
2111 let mut pe = PointerEvent::new(
2112 PointerId(0),
2113 PointerKind::Mouse,
2114 PointerEventKind::Enter,
2115 pos,
2116 1.0,
2117 modifiers,
2118 );
2119 pe.origin = Vec2 {
2120 x: h.rect.x,
2121 y: h.rect.y,
2122 };
2123 pe.position = pe.position - pe.origin;
2124 cb(pe);
2125 }
2126
2127 *hover_id = new_hover;
2128}
2129
2130fn hover_chain_for(frame: Option<&Frame>, hover: Option<u64>) -> std::collections::HashSet<u64> {
2131 let Some(f) = frame else {
2132 return std::collections::HashSet::new();
2133 };
2134 let Some(mut cur) = hover else {
2135 return std::collections::HashSet::new();
2136 };
2137 let map: std::collections::HashMap<u64, Option<u64>> =
2138 f.hit_regions.iter().map(|h| (h.id, h.parent)).collect();
2139 let mut set = std::collections::HashSet::new();
2140 loop {
2141 set.insert(cur);
2142 if let Some(Some(parent)) = map.get(&cur).copied() {
2143 cur = parent;
2144 } else {
2145 break;
2146 }
2147 }
2148 set
2149}
2150
2151fn dispatch_hover_change_bubbled(
2152 frame: Option<&Frame>,
2153 leave_map: &HashMap<u64, (f32, f32, f32, f32, Rc<dyn Fn(PointerEvent)>)>,
2154 hover_id: &mut Option<u64>,
2155 hover_ancestors: &mut std::collections::HashSet<u64>,
2156 new_hover: Option<u64>,
2157 pos: Vec2,
2158 modifiers: Modifiers,
2159) {
2160 let old_hover = *hover_id;
2161 let old_chain = hover_chain_for(frame, old_hover);
2162 let new_chain = hover_chain_for(frame, new_hover);
2163 if old_chain == new_chain {
2164 return;
2165 }
2166 for leave_id in old_chain.difference(&new_chain) {
2167 let leave_info = leave_map.get(leave_id).cloned().or_else(|| {
2168 frame.and_then(|f| {
2169 f.hit_regions
2170 .iter()
2171 .find(|h| h.id == *leave_id)
2172 .and_then(|h| {
2173 h.on_pointer_leave
2174 .as_ref()
2175 .map(|cb| (h.rect.x, h.rect.y, h.rect.w, h.rect.h, cb.clone()))
2176 })
2177 })
2178 });
2179 if let Some((rx, ry, _rw, _rh, cb)) = leave_info {
2180 let mut pe = PointerEvent::new(
2181 PointerId(0),
2182 PointerKind::Mouse,
2183 PointerEventKind::Leave,
2184 pos,
2185 1.0,
2186 modifiers,
2187 );
2188 pe.origin = Vec2 { x: rx, y: ry };
2189 pe.position = pe.position - pe.origin;
2190 cb(pe);
2191 }
2192 }
2193 for enter_id in new_chain.difference(&old_chain) {
2194 if let Some(f) = frame
2195 && let Some(h) = f.hit_regions.iter().find(|h| h.id == *enter_id)
2196 && let Some(cb) = &h.on_pointer_enter
2197 {
2198 let mut pe = PointerEvent::new(
2199 PointerId(0),
2200 PointerKind::Mouse,
2201 PointerEventKind::Enter,
2202 pos,
2203 1.0,
2204 modifiers,
2205 );
2206 pe.origin = Vec2 {
2207 x: h.rect.x,
2208 y: h.rect.y,
2209 };
2210 pe.position = pe.position - pe.origin;
2211 cb(pe);
2212 }
2213 }
2214 *hover_id = new_hover;
2215 hover_ancestors.clear();
2216 for id in &new_chain {
2217 if Some(*id) != new_hover {
2218 hover_ancestors.insert(*id);
2219 }
2220 }
2221}
2222
2223fn is_textfield_in_frame(f: &Frame, id: u64) -> bool {
2224 f.semantics_nodes
2225 .iter()
2226 .any(|n| n.id == id && n.role == repose_core::semantics::Role::TextField)
2227}
2228
2229fn is_multiline_id(f: &Frame, id: u64) -> bool {
2230 f.hit_regions
2231 .iter()
2232 .find(|h| h.id == id)
2233 .map(|h| h.tf_multiline)
2234 .unwrap_or(false)
2235}
2236
2237fn tf_can_edit(hit: &HitRegion) -> bool {
2240 hit.tf_enabled && !hit.tf_read_only
2241}
2242
2243fn is_tf_editable(f: &Frame, id: u64) -> bool {
2244 f.hit_regions
2245 .iter()
2246 .find(|h| h.id == id)
2247 .is_some_and(tf_can_edit)
2248}
2249
2250fn tf_key_of(frame: &Frame, visual_id: u64) -> u64 {
2251 if let Some(i) = frame.hit_regions.iter().position(|h| h.id == visual_id) {
2252 let hr = &frame.hit_regions[i];
2253 return hr.tf_state_key.unwrap_or(hr.id);
2254 }
2255 visual_id
2256}
2257
2258fn notify_text_change(f: &Frame, id: u64, text: String) {
2259 if let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
2260 && let Some(cb) = &h.on_text_change
2261 {
2262 cb(text);
2263 }
2264}
2265
2266fn tf_ensure_caret_visible(state: &mut TextFieldState, is_multiline: bool) {
2267 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
2268 let wrap_width = state.inner_width;
2269
2270 if is_multiline {
2271 let (cx, cy, _) = caret_xy_for_byte(&state.text, font_px, wrap_width, state.caret_index());
2272 state.ensure_caret_visible_xy(cx, cy, state.inner_width, state.inner_height, dp_to_px(2.0));
2273 } else {
2274 let caret_idx = state.caret_index();
2275 let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
2276 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
2277 let tfmd = vt.filter(&annotated);
2278 let off =
2279 repose_core::original_offset_to_display(&state.text, tfmd.text.as_str(), caret_idx);
2280 (tfmd.text.text, off)
2281 } else {
2282 (state.text.clone(), caret_idx)
2283 };
2284 let m = measure_text(&display, font_px, TextMeasureConfig::default());
2285 let caret_x_px = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
2286 state.ensure_caret_visible(caret_x_px, wrap_width, dp_to_px(2.0));
2287 }
2288}
2289
2290fn index_for_x_bytes_vt(state: &TextFieldState, font_px: f32, x_px: f32) -> usize {
2291 if let Some(vt) = &state.visual_transformation {
2292 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
2293 let tfmd = vt.filter(&annotated);
2294 let display_idx =
2295 repose_ui::textfield::index_for_x_bytes(tfmd.text.as_str(), font_px, x_px, 400, 0);
2296 tfmd.offset_mapping.transformed_to_original(display_idx)
2297 } else {
2298 repose_ui::textfield::index_for_x_bytes(&state.text, font_px, x_px, 400, 0)
2299 }
2300}
2301
2302fn index_for_xy_bytes_vt(
2303 state: &TextFieldState,
2304 font_px: f32,
2305 wrap_w: f32,
2306 x_px: f32,
2307 y_px: f32,
2308) -> usize {
2309 if let Some(vt) = &state.visual_transformation {
2310 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
2311 let tfmd = vt.filter(&annotated);
2312 let display_idx = repose_ui::textfield::index_for_xy_bytes(
2313 tfmd.text.as_str(),
2314 font_px,
2315 wrap_w,
2316 x_px,
2317 y_px,
2318 );
2319 tfmd.offset_mapping.transformed_to_original(display_idx)
2320 } else {
2321 repose_ui::textfield::index_for_xy_bytes(&state.text, font_px, wrap_w, x_px, y_px)
2322 }
2323}
2324
2325fn dispatch_scroll(
2327 frame: &Frame,
2328 pos: Vec2,
2329 delta: Vec2,
2330 scroll_capture: Option<u64>,
2331) -> (bool, Option<u64>) {
2332 if let Some(cid) = scroll_capture
2333 && let Some(cb) = frame
2334 .hit_regions
2335 .iter()
2336 .find(|h| h.id == cid)
2337 .and_then(|h| h.on_scroll.as_ref())
2338 {
2339 cb(delta);
2340 return (true, Some(cid));
2341 }
2342 let mut remaining = delta;
2345 for hit in frame
2346 .hit_regions
2347 .iter()
2348 .rev()
2349 .filter(|h| h.rect.contains(pos))
2350 {
2351 if let Some(cb) = &hit.on_scroll {
2352 let before = remaining;
2353 let leftover = cb(before);
2354 let consumed =
2355 (before.x - leftover.x).abs() > 0.001 || (before.y - leftover.y).abs() > 0.001;
2356 if consumed {
2357 return (true, Some(hit.id));
2358 }
2359 remaining = leftover;
2360 if remaining.x.abs() <= 0.001 && remaining.y.abs() <= 0.001 {
2361 break;
2362 }
2363 }
2364 }
2365 (false, scroll_capture)
2366}