1use std::cell::RefCell;
2use std::collections::{HashMap, HashSet};
3use std::rc::Rc;
4
5use repose_core::dnd;
6use repose_core::shortcuts::DragAction;
7use repose_core::input::{
8 ImeEvent, Key, KeyEvent, KeyEventType, Modifiers, PointerButton, PointerEvent,
9 PointerEventKind, PointerId, PointerKind,
10};
11use repose_core::locals::{dp_to_px, set_density_default, with_density, Density};
12use repose_core::runtime::{Frame, Scheduler};
13use repose_core::{
14 take_focus_request, CursorIcon, HitRegion, RenderContext, Scene, Vec2, View,
15 request_frame,
16};
17use repose_ui::textfield::{
18 caret_xy_for_byte, measure_text, TextFieldState, TF_FONT_DP, TextMeasureConfig,
19};
20use repose_ui::{layout_and_paint, Interactions};
21
22#[derive(Clone, Default)]
24pub struct PlatformOutput {
25 pub cursor: Option<CursorIcon>,
27 pub ime_allowed: bool,
29 pub ime_cursor_area: Option<(f64, f64, f64, f64)>,
31 pub clipboard_text: Option<String>,
33}
34
35pub struct FrameOutput {
37 pub scene: Scene,
39 pub hit_regions: Vec<HitRegion>,
41 pub semantics_nodes: Vec<repose_core::runtime::SemNode>,
43 pub focus_chain: Vec<u64>,
45 pub platform: PlatformOutput,
47 pub wants_pointer: bool,
49 pub wants_keyboard: bool,
51}
52
53pub struct PointerMoveResult {
55 pub cursor: Option<CursorIcon>,
57 pub hover_id: Option<u64>,
59}
60
61pub struct PointerButtonResult {
63 pub focused: Option<u64>,
65 pub capture_id: Option<u64>,
67 pub consumed: bool,
69 pub needs_a11y_announce: bool,
71}
72
73pub struct ReposeRuntime {
79 pub sched: Scheduler,
80 pub scale: f32,
81
82 pub modifiers: Modifiers,
84 pub mouse_pos_px: (f32, f32),
85 pub pointer_inside: bool,
87 pub hover_id: Option<u64>,
88 pub capture_id: Option<u64>,
89 pub scroll_capture_id: Option<u64>,
91 last_scroll_at: Option<web_time::Instant>,
92 pub pressed_ids: HashSet<u64>,
93 pub ime_preedit: bool,
94 pub key_pressed_active: Option<u64>,
95 pub last_focus: Option<u64>,
96
97 pub frame_cache: Option<Frame>,
99
100 cursor: Option<CursorIcon>,
102
103 pub textfield_states: HashMap<u64, Rc<RefCell<TextFieldState>>>,
105}
106
107impl ReposeRuntime {
108 pub fn new() -> Self {
109 Self {
110 sched: Scheduler::new(),
111 scale: 1.0,
112 modifiers: Modifiers::default(),
113 mouse_pos_px: (0.0, 0.0),
114 pointer_inside: false,
115 hover_id: None,
116 capture_id: None,
117 scroll_capture_id: None,
118 last_scroll_at: None,
119 pressed_ids: HashSet::new(),
120 ime_preedit: false,
121 key_pressed_active: None,
122 last_focus: None,
123 frame_cache: None,
124 cursor: None,
125 textfield_states: HashMap::new(),
126 }
127 }
128
129
130 pub fn set_viewport(&mut self, width_px: u32, height_px: u32) {
132 self.sched.size = (width_px, height_px);
133 }
134
135 pub fn set_viewport_and_scale(&mut self, width_px: u32, height_px: u32, scale: f32) {
137 self.scale = scale;
138 self.sched.size = (width_px, height_px);
139 }
140
141 pub fn tick_animations(&self) {
143 repose_core::animation_driver::tick();
144 }
145
146
147 pub fn compose<F>(
152 &mut self,
153 root_fn: &mut F,
154 render_ctx: &RenderContext,
155 ) -> Frame
156 where
157 F: FnMut(&mut Scheduler, &RenderContext) -> View,
158 {
159 let size = self.sched.size;
160 let rc = render_ctx.clone();
161 let mut inner = |s: &mut Scheduler| (root_fn)(s, &rc);
162 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
165 compose_frame_inner(
166 &mut self.sched,
167 &mut inner,
168 self.scale,
169 size,
170 self.hover_id,
171 &self.pressed_ids,
172 &self.textfield_states,
173 )
174 })) {
175 Ok(frame) => frame,
176 Err(_) => {
177 log::error!("compose panicked; presenting last good frame");
178 self.frame_cache.clone().unwrap_or_else(|| Frame {
179 scene: Default::default(),
180 hit_regions: Vec::new(),
181 semantics_nodes: Vec::new(),
182 focus_chain: Vec::new(),
183 })
184 }
185 }
186 }
187
188 pub fn frame(
190 &mut self,
191 mut root_fn: impl FnMut(&mut Scheduler, &RenderContext) -> View,
192 render_ctx: &RenderContext,
193 ) -> FrameOutput {
194 let captured = Rc::new(RefCell::new(None::<String>));
195 let hook = captured.clone();
196 repose_core::clipboard::set_clipboard_observer(Box::new(move |text| {
197 *hook.borrow_mut() = Some(text.to_string());
198 }));
199
200 let f = self.compose(&mut root_fn, render_ctx);
201
202 repose_core::clipboard::clear_clipboard_observer();
203 let clipboard_text = captured.borrow_mut().take();
204
205 let wants_pointer = !f.hit_regions.is_empty() || self.hover_id.is_some() || self.capture_id.is_some();
206 let wants_keyboard = !self.textfield_states.is_empty() || self.ime_preedit;
207
208 let ime_allowed = self.sched.focused.map_or(false, |fid| {
209 f.semantics_nodes
210 .iter()
211 .any(|n| n.id == fid && n.role == repose_core::semantics::Role::TextField)
212 });
213
214 let ime_cursor_area = if ime_allowed {
215 self.sched.focused.and_then(|fid| {
216 f.hit_regions.iter().find(|h| h.id == fid).map(|hit| {
217 let sf = self.scale as f64;
218 (
219 hit.rect.x as f64 / sf,
220 hit.rect.y as f64 / sf,
221 hit.rect.w as f64 / sf,
222 hit.rect.h as f64 / sf,
223 )
224 })
225 })
226 } else {
227 None
228 };
229
230 let platform = PlatformOutput {
231 cursor: self.take_cursor_suggestion(),
232 ime_allowed,
233 ime_cursor_area,
234 clipboard_text,
235 };
236 FrameOutput {
237 scene: f.scene,
238 hit_regions: f.hit_regions,
239 semantics_nodes: f.semantics_nodes,
240 focus_chain: f.focus_chain,
241 platform,
242 wants_pointer,
243 wants_keyboard,
244 }
245 }
246
247 pub fn cache_frame(&mut self, frame: Frame) {
249 self.frame_cache = Some(frame);
250 }
251
252
253 pub fn handle_pointer_move(&mut self, pos: Vec2) -> PointerMoveResult {
255 self.mouse_pos_px = (pos.x, pos.y);
256
257 if dnd::handle_drag_action(&DragAction::Move {
259 position: pos,
260 modifiers: self.modifiers,
261 }) {
262 request_frame();
263 return PointerMoveResult {
264 cursor: self.cursor,
265 hover_id: self.hover_id,
266 };
267 }
268
269 let Some(f) = &self.frame_cache else {
270 return PointerMoveResult {
271 cursor: None,
272 hover_id: None,
273 };
274 };
275
276 if let Some(cid) = self.capture_id {
278 if is_textfield_in_frame(f, cid) {
279 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
280 let key = tf_key_of(f, cid);
281 if let Some(st_rc) = self.textfield_states.get(&key) {
282 let mut st = st_rc.borrow_mut();
283 let (ox, oy) = hit.tf_content_origin.unwrap_or((hit.rect.x, hit.rect.y));
284 let content_x = (pos.x - ox + st.scroll_offset).max(0.0);
285 let content_y = (pos.y - oy + st.scroll_offset_y).max(0.0);
286 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
287 let wrap_w = st.inner_width.max(1.0);
288 let idx = if hit.tf_multiline {
289 index_for_xy_bytes_vt(&st, font_px, wrap_w, content_x, content_y)
290 } else {
291 index_for_x_bytes_vt(&st, font_px, content_x)
292 };
293 st.drag_to(idx);
294 }
295 }
296 }
297 }
298
299 let top = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos));
301
302 self.cursor = top
304 .and_then(|h| h.cursor)
305 .or(Some(CursorIcon::Default));
306
307 let new_hover = top.map(|h| h.id);
308
309 if new_hover != self.hover_id {
311 dispatch_hover_change(
312 Some(f),
313 &mut self.hover_id,
314 new_hover,
315 pos,
316 self.modifiers,
317 );
318 request_frame();
319 }
320
321 let pe = PointerEvent::new(
323 PointerId(0),
324 PointerKind::Mouse,
325 PointerEventKind::Move,
326 pos,
327 1.0,
328 self.modifiers,
329 );
330
331 if let Some(cid) = self.capture_id {
332 if let Some(h) = f.hit_regions.iter().find(|h| h.id == cid) {
333 if let Some(cb) = &h.on_pointer_move {
334 cb(pe);
335 }
336 }
337 } else if let Some(h) = top {
338 if let Some(cb) = &h.on_pointer_move {
339 cb(pe);
340 }
341 }
342
343 PointerMoveResult {
344 cursor: self.cursor,
345 hover_id: self.hover_id,
346 }
347 }
348
349 pub fn handle_pointer_press(
351 &mut self,
352 pos: Vec2,
353 button: PointerButton,
354 ) -> PointerButtonResult {
355 self.mouse_pos_px = (pos.x, pos.y);
356
357 let Some(f) = &self.frame_cache else {
358 return PointerButtonResult {
359 focused: None,
360 capture_id: None,
361 consumed: false,
362 needs_a11y_announce: false,
363 };
364 };
365
366 let mut result = PointerButtonResult {
367 focused: None,
368 capture_id: None,
369 consumed: false,
370 needs_a11y_announce: false,
371 };
372
373 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos)) {
374 dnd::handle_drag_action(&DragAction::Press {
376 position: pos,
377 capture_id: hit.id,
378 kind: PointerKind::Mouse,
379 modifiers: self.modifiers,
380 });
381
382 self.capture_id = Some(hit.id);
384 result.capture_id = Some(hit.id);
385 result.consumed = true;
386
387 if is_textfield_in_frame(f, hit.id) {
389 let key = tf_key_of(f, hit.id);
390 let st_rc = self
391 .textfield_states
392 .entry(key)
393 .or_insert_with(|| Rc::new(RefCell::new(TextFieldState::new())));
394 let mut st = st_rc.borrow_mut();
395 let (ox, oy) = hit.tf_content_origin.unwrap_or((hit.rect.x, hit.rect.y));
396 let content_x = (pos.x - ox + st.scroll_offset).max(0.0);
397 let content_y = (pos.y - oy + st.scroll_offset_y).max(0.0);
398 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
399 let wrap_w = st.inner_width.max(1.0);
400
401 let idx = if hit.tf_multiline {
402 index_for_xy_bytes_vt(&st, font_px, wrap_w, content_x, content_y)
403 } else {
404 index_for_x_bytes_vt(&st, font_px, content_x)
405 };
406 st.handle_pointer_down(idx, (pos.x, pos.y), self.modifiers.shift);
407 }
408
409 self.pressed_ids.insert(hit.id);
411
412 if hit.focusable {
414 self.sched.focused = Some(hit.id);
415 result.focused = Some(hit.id);
416 let key = tf_key_of(f, hit.id);
417 self.textfield_states.entry(key).or_insert_with(|| {
418 Rc::new(RefCell::new(TextFieldState::new()))
419 });
420 }
421
422 if let Some(cb) = &hit.on_pointer_down {
424 let pe = PointerEvent::new(
425 PointerId(0),
426 PointerKind::Mouse,
427 PointerEventKind::Down(button),
428 pos,
429 1.0,
430 self.modifiers,
431 );
432 cb(pe);
433 }
434
435 request_frame();
436 } else {
437 if self.ime_preedit {
439 self.ime_preedit = false;
440 }
441 self.sched.focused = None;
442 request_frame();
443 }
444
445 result
446 }
447
448 pub fn handle_pointer_release(&mut self, pos: Vec2, _button: PointerButton) {
450 self.mouse_pos_px = (pos.x, pos.y);
451
452 if dnd::handle_drag_action(&DragAction::Release {
453 position: pos,
454 modifiers: self.modifiers,
455 }) {
456 self.capture_id = None;
457 self.pressed_ids.clear();
458 request_frame();
459 return;
460 }
461
462 if let Some(cid) = self.capture_id {
463 self.pressed_ids.remove(&cid);
464 }
465
466 let Some(f) = &self.frame_cache else {
467 self.capture_id = None;
468 return;
469 };
470
471 if let Some(cid) = self.capture_id {
473 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
474 if let Some(cb) = &hit.on_pointer_up {
475 let pe = PointerEvent::new(
476 PointerId(0),
477 PointerKind::Mouse,
478 PointerEventKind::Up(_button),
479 pos,
480 1.0,
481 self.modifiers,
482 );
483 cb(pe);
484 }
485 }
486 }
487
488 if let Some(cid) = self.capture_id {
490 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
491 if hit.rect.contains(pos) {
492 if let Some(cb) = &hit.on_click {
493 cb();
494 }
495 }
496 }
497 }
498
499 if let Some(cid) = self.capture_id {
501 if is_semantics_textfield(f, cid) {
502 let key = tf_key_of(f, cid);
503 if let Some(state_rc) = self.textfield_states.get(&key) {
504 state_rc.borrow_mut().end_drag();
505 }
506 }
507 }
508
509 self.capture_id = None;
510 request_frame();
511 }
512
513 pub fn handle_pointer_cancel(&mut self) {
515 dnd::handle_drag_action(&DragAction::Cancel);
516 let pos = Vec2 {
517 x: self.mouse_pos_px.0,
518 y: self.mouse_pos_px.1,
519 };
520 dispatch_hover_change(
521 self.frame_cache.as_ref(),
522 &mut self.hover_id,
523 None,
524 pos,
525 self.modifiers,
526 );
527 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
529 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
530 if let Some(cb) = &hit.on_pointer_cancel {
531 let pe = PointerEvent::new(
532 PointerId(0),
533 PointerKind::Mouse,
534 PointerEventKind::Cancel,
535 pos,
536 1.0,
537 self.modifiers,
538 );
539 cb(pe);
540 }
541 }
542 }
543 self.reset_pointer_state();
544 }
545
546 pub fn clear_hover(&mut self) {
548 if self.hover_id.is_none() {
549 return;
550 }
551 let pos = Vec2 {
552 x: self.mouse_pos_px.0,
553 y: self.mouse_pos_px.1,
554 };
555 dispatch_hover_change(
556 self.frame_cache.as_ref(),
557 &mut self.hover_id,
558 None,
559 pos,
560 self.modifiers,
561 );
562 }
563
564 pub fn reconcile_hover_from_mouse_pos(&mut self, new_frame: &Frame) {
566 let mut changed = false;
567
568 if let Some(prev_id) = self.hover_id {
569 if !new_frame.hit_regions.iter().any(|h| h.id == prev_id) {
570 if let Some(old_f) = &self.frame_cache
571 && let Some(prev) = old_f.hit_regions.iter().find(|h| h.id == prev_id)
572 && let Some(cb) = &prev.on_pointer_leave
573 {
574 let pos = Vec2 {
575 x: self.mouse_pos_px.0,
576 y: self.mouse_pos_px.1,
577 };
578 let pe = PointerEvent::new(
579 PointerId(0),
580 PointerKind::Mouse,
581 PointerEventKind::Leave,
582 pos,
583 1.0,
584 self.modifiers,
585 );
586 cb(pe);
587 changed = true;
588 }
589 self.hover_id = None;
590 }
591 }
592
593 if !self.pointer_inside {
594 return;
595 }
596
597 let pos = Vec2 {
598 x: self.mouse_pos_px.0,
599 y: self.mouse_pos_px.1,
600 };
601 let new_hover = new_frame
602 .hit_regions
603 .iter()
604 .rev()
605 .find(|h| h.rect.contains(pos))
606 .map(|h| h.id);
607
608 if new_hover == self.hover_id {
609 if changed {
610 request_frame();
611 }
612 return;
613 }
614
615 dispatch_hover_change(
616 Some(new_frame),
617 &mut self.hover_id,
618 new_hover,
619 pos,
620 self.modifiers,
621 );
622 request_frame();
623 }
624
625 fn reset_pointer_state(&mut self) {
626 self.capture_id = None;
627 self.pressed_ids.clear();
628 self.hover_id = None;
629 }
630
631
632 pub fn handle_scroll(&mut self, delta: Vec2) -> bool {
634 let Some(f) = &self.frame_cache else {
635 return false;
636 };
637
638 let now = web_time::Instant::now();
639 if let Some(last) = self.last_scroll_at {
640 if now.duration_since(last).as_millis() > 250 {
641 self.scroll_capture_id = None;
642 }
643 }
644 self.last_scroll_at = Some(now);
645
646 let pos = Vec2 {
647 x: self.mouse_pos_px.0,
648 y: self.mouse_pos_px.1,
649 };
650 let (consumed, cap) = dispatch_scroll(f, pos, delta, self.scroll_capture_id);
651 self.scroll_capture_id = cap;
652 if consumed {
653 request_frame();
654 }
655 consumed
656 }
657
658
659 pub fn handle_key(&mut self, event: &KeyEvent) -> bool {
661 let Some(f) = &self.frame_cache else {
662 return false;
663 };
664
665 if event.event_type == KeyEventType::Down && !event.is_repeat {
667 if event.key == Key::Escape {
668 if dnd::handle_drag_action(&DragAction::Cancel) {
669 request_frame();
670 return true;
671 }
672 if self.dispatch_focus_key_event(f, event) {
674 request_frame();
675 return true;
676 }
677 return true;
678 }
679 }
680
681 let consumed = self.dispatch_focus_key_event(f, event);
683 if consumed {
684 request_frame();
685 return true;
686 }
687
688 if event.event_type == KeyEventType::Down && !event.is_repeat {
690 if let Some(action) = repose_core::shortcuts::resolve_action(
691 repose_core::shortcuts::KeyChord::new(event.key.clone(), self.modifiers),
692 ) {
693 if self.dispatch_action(f, action.clone()) {
694 request_frame();
695 return true;
696 }
697 if let Some(new_id) = repose_core::focus::handle_action(&action, &mut self.sched, f)
699 {
700 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == new_id) {
702 if let Some(key) = hit.tf_state_key {
703 self.textfield_states.entry(key).or_insert_with(|| {
704 Rc::new(RefCell::new(TextFieldState::new()))
705 });
706 }
707 }
708 request_frame();
709 return true;
710 }
711 }
712 }
713
714 if let Some(fid) = self.sched.focused {
716 let is_tf = f.semantics_nodes.iter().any(|n| n.id == fid && n.role == repose_core::semantics::Role::TextField);
717 if !is_tf {
718 if event.event_type == KeyEventType::Down && !event.is_repeat {
719 if event.key == Key::Space || event.key == Key::Enter {
720 self.pressed_ids.insert(fid);
721 self.key_pressed_active = Some(fid);
722 request_frame();
723 return true;
724 }
725 } else if event.event_type == KeyEventType::Up {
726 if let Some(active_id) = self.key_pressed_active {
727 if event.key == Key::Space || event.key == Key::Enter {
728 self.pressed_ids.remove(&active_id);
729 self.key_pressed_active = None;
730 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == active_id) {
731 if let Some(cb) = &hit.on_click {
732 cb();
733 } else if let Some(cb) = &hit.on_pointer_down {
734 let pe = PointerEvent::new(
735 PointerId(0),
736 PointerKind::Mouse,
737 PointerEventKind::Down(PointerButton::Primary),
738 Vec2 { x: 0.0, y: 0.0 },
739 1.0,
740 self.modifiers,
741 );
742 cb(pe);
743 }
744 }
745 request_frame();
746 return true;
747 }
748 }
749 }
750 }
751 }
752
753 if event.event_type == KeyEventType::Down && !event.is_repeat && event.key == Key::Enter {
755 if let Some(fid) = self.sched.focused {
756 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
757 let is_multiline = hit.tf_multiline;
758 let should_submit = if is_multiline {
759 self.modifiers.ctrl || self.modifiers.meta
760 } else {
761 true
762 };
763 if should_submit {
764 if let Some(on_submit) = &hit.on_text_submit {
765 let key = tf_key_of(f, fid);
766 if let Some(state_rc) = self.textfield_states.get(&key) {
767 let text = state_rc.borrow().text.clone();
768 on_submit(text);
769 request_frame();
770 return true;
771 }
772 }
773 } else {
774 let key = tf_key_of(f, fid);
776 if let Some(state_rc) = self.textfield_states.get(&key) {
777 let mut st = state_rc.borrow_mut();
778 st.insert_text("\n");
779 let new_text = st.text.clone();
780 notify_text_change(f, fid, new_text);
781 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
782 request_frame();
783 return true;
784 }
785 }
786 }
787 }
788 }
789
790 if event.event_type == KeyEventType::Down {
792 if let Some(fid) = self.sched.focused {
793 let key = tf_key_of(f, fid);
794 if let Some(state_rc) = self.textfield_states.get(&key) {
795 let mut state = state_rc.borrow_mut();
796 match event.key {
797 Key::Backspace => {
798 state.delete_backward();
799 let new_text = state.text.clone();
800 notify_text_change(f, fid, new_text);
801 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
802 request_frame();
803 return true;
804 }
805 Key::Delete => {
806 state.delete_forward();
807 let new_text = state.text.clone();
808 notify_text_change(f, fid, new_text);
809 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
810 request_frame();
811 return true;
812 }
813 Key::ArrowLeft => {
814 state.move_cursor(-1, self.modifiers.shift);
815 state.preferred_x_px = None;
816 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
817 request_frame();
818 return true;
819 }
820 Key::ArrowRight => {
821 state.move_cursor(1, self.modifiers.shift);
822 state.preferred_x_px = None;
823 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
824 request_frame();
825 return true;
826 }
827 Key::ArrowUp => {
828 if is_multiline_id(f, fid) {
829 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
830 let font_px = dp_to_px(TF_FONT_DP);
831 let cur = state.caret_index();
832 let (new_pos, px) =
833 repose_ui::textfield::move_caret_vertical(
834 &state.text,
835 font_px,
836 hit.rect.w,
837 cur,
838 -1,
839 state.preferred_x_px,
840 );
841 if self.modifiers.shift {
842 state.selection.end = new_pos;
843 } else {
844 state.selection = new_pos..new_pos;
845 }
846 state.preferred_x_px = Some(px);
847 let (cx, cy, _) = caret_xy_for_byte(
848 &state.text,
849 font_px,
850 hit.rect.w,
851 state.caret_index(),
852 );
853 let iw = state.inner_width;
854 let ih = state.inner_height;
855 state.ensure_caret_visible_xy(
856 cx, cy,
857 iw,
858 ih,
859 dp_to_px(2.0),
860 );
861 request_frame();
862 return true;
863 }
864 }
865 }
866 Key::ArrowDown => {
867 if is_multiline_id(f, fid) {
868 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
869 let font_px = dp_to_px(TF_FONT_DP);
870 let cur = state.caret_index();
871 let (new_pos, px) =
872 repose_ui::textfield::move_caret_vertical(
873 &state.text,
874 font_px,
875 hit.rect.w,
876 cur,
877 1,
878 state.preferred_x_px,
879 );
880 if self.modifiers.shift {
881 state.selection.end = new_pos;
882 } else {
883 state.selection = new_pos..new_pos;
884 }
885 state.preferred_x_px = Some(px);
886 let (cx, cy, _) = caret_xy_for_byte(
887 &state.text,
888 font_px,
889 hit.rect.w,
890 state.caret_index(),
891 );
892 let iw = state.inner_width;
893 let ih = state.inner_height;
894 state.ensure_caret_visible_xy(
895 cx, cy,
896 iw,
897 ih,
898 dp_to_px(2.0),
899 );
900 request_frame();
901 return true;
902 }
903 }
904 }
905 Key::Home => {
906 state.selection = 0..0;
907 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
908 request_frame();
909 return true;
910 }
911 Key::End => {
912 let end = state.text.len();
913 state.selection = end..end;
914 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
915 request_frame();
916 return true;
917 }
918 _ => {}
919 }
920 }
921 }
922
923 if !self.ime_preedit
925 && !self.modifiers.ctrl
926 && !self.modifiers.alt
927 && !self.modifiers.meta
928 {
929 if let Key::Character(c) = event.key {
930 if !c.is_control() && c != '\n' && c != '\r' {
931 if let Some(fid) = self.sched.focused {
932 let key = tf_key_of(f, fid);
933 if let Some(state_rc) = self.textfield_states.get(&key) {
934 let mut st = state_rc.borrow_mut();
935 let text = c.to_string();
936 st.insert_text(&text);
937 notify_text_change(f, fid, st.text.clone());
938 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
939 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
940 }
941 request_frame();
942 return true;
943 }
944 }
945 }
946 }
947 }
948 }
949
950 if event.event_type == KeyEventType::Up {
952 if let Some(active_id) = self.key_pressed_active {
953 if event.key == Key::Space || event.key == Key::Enter {
954 self.pressed_ids.remove(&active_id);
955 self.key_pressed_active = None;
956 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == active_id) {
957 if let Some(cb) = &hit.on_click {
958 cb();
959 }
960 }
961 request_frame();
962 return true;
963 }
964 }
965 }
966
967 false
968 }
969
970 fn dispatch_focus_key_event(&self, f: &Frame, event: &KeyEvent) -> bool {
972 let Some(focused) = self.sched.focused else {
973 return false;
974 };
975
976 let hit_by_id: HashMap<u64, &HitRegion> =
977 f.hit_regions.iter().map(|h| (h.id, h)).collect();
978 let sem_parent_of: HashMap<u64, u64> = f
979 .semantics_nodes
980 .iter()
981 .filter_map(|n| n.parent.map(|p| (n.id, p)))
982 .collect();
983
984 let mut ancestors = Vec::new();
986 let mut cur = focused;
987 loop {
988 ancestors.push(cur);
989 if let Some(&p) = sem_parent_of.get(&cur) {
990 cur = p;
991 } else {
992 break;
993 }
994 }
995
996 for &id in ancestors.iter().rev() {
998 if let Some(hit) = hit_by_id.get(&id) {
999 if let Some(cb) = &hit.on_preview_key_event {
1000 if cb(event.clone()) {
1001 return true;
1002 }
1003 }
1004 }
1005 }
1006
1007 for &id in ancestors.iter() {
1009 if let Some(hit) = hit_by_id.get(&id) {
1010 if let Some(cb) = &hit.on_key_event {
1011 if cb(event.clone()) {
1012 return true;
1013 }
1014 }
1015 }
1016 }
1017
1018 false
1019 }
1020
1021 fn dispatch_action(&self, f: &Frame, action: repose_core::shortcuts::Action) -> bool {
1023 if let Some(fid) = self.sched.focused {
1024 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1025 if let Some(cb) = &hit.on_action {
1026 if cb(action.clone()) {
1027 return true;
1028 }
1029 }
1030 }
1031 }
1032
1033 if repose_core::shortcuts::handle(action.clone()) {
1034 return true;
1035 }
1036
1037 false
1038 }
1039
1040
1041 pub fn handle_ime(&mut self, event: &ImeEvent) {
1043 let Some(fid) = self.sched.focused else {
1044 return;
1045 };
1046 let Some(f) = &self.frame_cache else {
1047 return;
1048 };
1049 let key = tf_key_of(f, fid);
1050 let Some(state_rc) = self.textfield_states.get(&key) else {
1051 return;
1052 };
1053
1054 let mut state = state_rc.borrow_mut();
1055
1056 match event {
1057 ImeEvent::Start => {
1058 self.ime_preedit = false;
1059 }
1060 ImeEvent::Update { text, cursor } => {
1061 state.set_composition(text.clone(), *cursor);
1062 self.ime_preedit = !text.is_empty();
1063 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1064 notify_text_change(f, fid, state.text.clone());
1065 }
1066 ImeEvent::Commit(text) => {
1067 state.commit_composition(text.clone());
1068 self.ime_preedit = false;
1069 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1070 notify_text_change(f, fid, state.text.clone());
1071 }
1072 ImeEvent::Cancel => {
1073 self.ime_preedit = false;
1074 if state.composition.is_some() {
1075 state.cancel_composition();
1076 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1077 notify_text_change(f, fid, state.text.clone());
1078 }
1079 }
1080 }
1081
1082 request_frame();
1083 }
1084
1085
1086 pub fn handle_focus_lost(&mut self) {
1088 dnd::handle_drag_action(&DragAction::Cancel);
1089 self.handle_pointer_cancel();
1090 self.ime_preedit = false;
1091 }
1092
1093
1094 pub fn ensure_textfield_state(&mut self, key: u64) -> Rc<RefCell<TextFieldState>> {
1096 self.textfield_states
1097 .entry(key)
1098 .or_insert_with(|| Rc::new(RefCell::new(TextFieldState::new())))
1099 .clone()
1100 }
1101
1102 pub fn tf_key_of(&self, visual_id: u64) -> u64 {
1104 self.frame_cache
1105 .as_ref()
1106 .map(|f| tf_key_of(f, visual_id))
1107 .unwrap_or(visual_id)
1108 }
1109
1110 pub fn is_textfield(&self, id: u64) -> bool {
1112 self.frame_cache
1113 .as_ref()
1114 .map(|f| is_textfield_in_frame(f, id))
1115 .unwrap_or(false)
1116 }
1117
1118 pub fn is_multiline(&self, id: u64) -> bool {
1120 self.frame_cache
1121 .as_ref()
1122 .map(|f| is_multiline_id(f, id))
1123 .unwrap_or(false)
1124 }
1125
1126
1127 pub fn paste_into_focused(&mut self, text: &str) {
1129 let Some(fid) = self.sched.focused else {
1130 return;
1131 };
1132 let Some(f) = &self.frame_cache.clone() else {
1133 return;
1134 };
1135 let key = tf_key_of(f, fid);
1136 if let Some(state_rc) = self.textfield_states.get(&key) {
1137 let mut st = state_rc.borrow_mut();
1138 st.insert_text_atomic(text);
1139 let new_text = st.text.clone();
1140 notify_text_change(f, fid, new_text);
1141 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1142 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1143 }
1144 }
1145 }
1146
1147 pub fn cursor_suggestion(&self) -> Option<CursorIcon> {
1149 self.cursor
1150 }
1151
1152 pub fn take_cursor_suggestion(&mut self) -> Option<CursorIcon> {
1154 self.cursor.take()
1155 }
1156}
1157
1158impl Default for ReposeRuntime {
1159 fn default() -> Self {
1160 Self::new()
1161 }
1162}
1163
1164
1165pub fn compose_frame_inner<F>(
1167 sched: &mut Scheduler,
1168 root_fn: &mut F,
1169 scale: f32,
1170 size_px_u32: (u32, u32),
1171 hover_id: Option<u64>,
1172 pressed_ids: &HashSet<u64>,
1173 tf_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
1174) -> Frame
1175where
1176 F: FnMut(&mut Scheduler) -> View,
1177{
1178 if let Some(requested_id) = take_focus_request() {
1179 if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
1180 sched.focused = None;
1181 } else {
1182 sched.focused = Some(requested_id);
1183 }
1184 }
1185
1186 set_density_default(Density { scale });
1187
1188 let current_focused = sched.focused;
1189
1190 let frame = sched.repose(
1191 {
1192 let scale = scale;
1193 move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s))
1194 },
1195 {
1196 let hover_id = hover_id;
1197 let pressed_ids = pressed_ids.clone();
1198 move |view, _size| {
1199 let interactions = Interactions {
1200 hover: hover_id,
1201 pressed: pressed_ids.clone(),
1202 };
1203 with_density(Density { scale }, || {
1204 layout_and_paint(view, size_px_u32, tf_states, &interactions, current_focused)
1205 })
1206 }
1207 },
1208 );
1209
1210 if let Some(fid) = sched.focused {
1211 if !frame.focus_chain.contains(&fid) {
1212 sched.focused = None;
1213 }
1214 }
1215
1216 frame
1217}
1218
1219fn dispatch_hover_change(
1223 frame: Option<&Frame>,
1224 hover_id: &mut Option<u64>,
1225 new_hover: Option<u64>,
1226 pos: Vec2,
1227 modifiers: Modifiers,
1228) {
1229 let Some(f) = frame else {
1230 *hover_id = None;
1231 return;
1232 };
1233 if new_hover == *hover_id {
1234 return;
1235 }
1236 if let Some(prev_id) = *hover_id {
1237 if let Some(prev) = f.hit_regions.iter().find(|h| h.id == prev_id) {
1238 if let Some(cb) = &prev.on_pointer_leave {
1239 let pe = PointerEvent::new(
1240 PointerId(0),
1241 PointerKind::Mouse,
1242 PointerEventKind::Leave,
1243 pos,
1244 1.0,
1245 modifiers,
1246 );
1247 cb(pe);
1248 }
1249 }
1250 }
1251 if let Some(hid) = new_hover {
1252 if let Some(h) = f.hit_regions.iter().find(|h| h.id == hid) {
1253 if let Some(cb) = &h.on_pointer_enter {
1254 let pe = PointerEvent::new(
1255 PointerId(0),
1256 PointerKind::Mouse,
1257 PointerEventKind::Enter,
1258 pos,
1259 1.0,
1260 modifiers,
1261 );
1262 cb(pe);
1263 }
1264 }
1265 }
1266 *hover_id = new_hover;
1267}
1268
1269fn is_textfield_in_frame(f: &Frame, id: u64) -> bool {
1270 f.semantics_nodes
1271 .iter()
1272 .any(|n| n.id == id && n.role == repose_core::semantics::Role::TextField)
1273}
1274
1275fn is_semantics_textfield(f: &Frame, id: u64) -> bool {
1276 f.semantics_nodes
1277 .iter()
1278 .any(|n| n.id == id && n.role == repose_core::semantics::Role::TextField)
1279}
1280
1281fn is_multiline_id(f: &Frame, id: u64) -> bool {
1282 f.hit_regions
1283 .iter()
1284 .find(|h| h.id == id)
1285 .map(|h| h.tf_multiline)
1286 .unwrap_or(false)
1287}
1288
1289fn tf_key_of(frame: &Frame, visual_id: u64) -> u64 {
1290 if let Some(i) = frame.hit_regions.iter().position(|h| h.id == visual_id) {
1291 let hr = &frame.hit_regions[i];
1292 return hr.tf_state_key.unwrap_or(hr.id);
1293 }
1294 visual_id
1295}
1296
1297fn notify_text_change(f: &Frame, id: u64, text: String) {
1298 if let Some(h) = f.hit_regions.iter().find(|h| h.id == id) {
1299 if let Some(cb) = &h.on_text_change {
1300 cb(text);
1301 }
1302 }
1303}
1304
1305fn tf_ensure_caret_visible(state: &mut TextFieldState, is_multiline: bool) {
1306 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
1307 let wrap_width = state.inner_width;
1308
1309 if is_multiline {
1310 let (cx, cy, _) = caret_xy_for_byte(&state.text, font_px, wrap_width, state.caret_index());
1311 state.ensure_caret_visible_xy(cx, cy, state.inner_width, state.inner_height, dp_to_px(2.0));
1312 } else {
1313 let caret_idx = state.caret_index();
1314 let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
1315 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
1316 let tfmd = vt.filter(&annotated);
1317 let off = repose_core::original_offset_to_display(&state.text, tfmd.text.as_str(), caret_idx);
1318 (tfmd.text.text, off)
1319 } else {
1320 (state.text.clone(), caret_idx)
1321 };
1322 let m = measure_text(&display, font_px, TextMeasureConfig::default());
1323 let caret_x_px = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
1324 state.ensure_caret_visible(caret_x_px, wrap_width, dp_to_px(2.0));
1325 }
1326}
1327
1328fn index_for_x_bytes_vt(state: &TextFieldState, font_px: f32, x_px: f32) -> usize {
1329 if let Some(vt) = &state.visual_transformation {
1330 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
1331 let tfmd = vt.filter(&annotated);
1332 let display_idx = repose_ui::textfield::index_for_x_bytes(tfmd.text.as_str(), font_px, x_px, 400, 0);
1333 tfmd.offset_mapping.transformed_to_original(display_idx)
1334 } else {
1335 repose_ui::textfield::index_for_x_bytes(&state.text, font_px, x_px, 400, 0)
1336 }
1337}
1338
1339fn index_for_xy_bytes_vt(
1340 state: &TextFieldState,
1341 font_px: f32,
1342 wrap_w: f32,
1343 x_px: f32,
1344 y_px: f32,
1345) -> usize {
1346 if let Some(vt) = &state.visual_transformation {
1347 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
1348 let tfmd = vt.filter(&annotated);
1349 let display_idx = repose_ui::textfield::index_for_xy_bytes(tfmd.text.as_str(), font_px, wrap_w, x_px, y_px);
1350 tfmd.offset_mapping.transformed_to_original(display_idx)
1351 } else {
1352 repose_ui::textfield::index_for_xy_bytes(&state.text, font_px, wrap_w, x_px, y_px)
1353 }
1354}
1355
1356fn dispatch_scroll(
1358 frame: &Frame,
1359 pos: Vec2,
1360 delta: Vec2,
1361 scroll_capture: Option<u64>,
1362) -> (bool, Option<u64>) {
1363 if let Some(cid) = scroll_capture {
1364 if let Some(cb) = frame
1365 .hit_regions
1366 .iter()
1367 .find(|h| h.id == cid)
1368 .and_then(|h| h.on_scroll.as_ref())
1369 {
1370 cb(delta);
1371 return (true, Some(cid));
1372 }
1373 }
1375
1376 let mut remaining = delta;
1377 for hit in frame
1378 .hit_regions
1379 .iter()
1380 .rev()
1381 .filter(|h| h.rect.contains(pos))
1382 {
1383 if let Some(cb) = &hit.on_scroll {
1384 let before = remaining;
1385 let leftover = cb(before);
1386 let consumed = (before.x - leftover.x).abs() > 0.001
1387 || (before.y - leftover.y).abs() > 0.001;
1388 if consumed {
1389 return (true, Some(hit.id));
1390 }
1391 remaining = leftover;
1392 if remaining.x.abs() <= 0.001 && remaining.y.abs() <= 0.001 {
1393 break;
1394 }
1395 }
1396 }
1397 (false, scroll_capture)
1398}