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 self.cursor = new_hover
609 .and_then(|id| new_frame.hit_regions.iter().find(|h| h.id == id))
610 .and_then(|h| h.cursor)
611 .or(Some(CursorIcon::Default));
612
613 if new_hover == self.hover_id {
614 if changed {
615 request_frame();
616 }
617 return;
618 }
619
620 dispatch_hover_change(
621 Some(new_frame),
622 &mut self.hover_id,
623 new_hover,
624 pos,
625 self.modifiers,
626 );
627 request_frame();
628 }
629
630 fn reset_pointer_state(&mut self) {
631 self.capture_id = None;
632 self.pressed_ids.clear();
633 self.hover_id = None;
634 }
635
636
637 pub fn handle_scroll(&mut self, delta: Vec2) -> bool {
639 let Some(f) = &self.frame_cache else {
640 return false;
641 };
642
643 let now = web_time::Instant::now();
644 if let Some(last) = self.last_scroll_at {
645 if now.duration_since(last).as_millis() > 250 {
646 self.scroll_capture_id = None;
647 }
648 }
649 self.last_scroll_at = Some(now);
650
651 let pos = Vec2 {
652 x: self.mouse_pos_px.0,
653 y: self.mouse_pos_px.1,
654 };
655 let (consumed, cap) = dispatch_scroll(f, pos, delta, self.scroll_capture_id);
656 self.scroll_capture_id = cap;
657 if consumed {
658 request_frame();
659 }
660 consumed
661 }
662
663
664 pub fn handle_key(&mut self, event: &KeyEvent) -> bool {
666 let Some(f) = &self.frame_cache else {
667 return false;
668 };
669
670 if event.event_type == KeyEventType::Down && !event.is_repeat {
672 if event.key == Key::Escape {
673 if dnd::handle_drag_action(&DragAction::Cancel) {
674 request_frame();
675 return true;
676 }
677 if self.dispatch_focus_key_event(f, event) {
679 request_frame();
680 return true;
681 }
682 return true;
683 }
684 }
685
686 let consumed = self.dispatch_focus_key_event(f, event);
688 if consumed {
689 request_frame();
690 return true;
691 }
692
693 if event.event_type == KeyEventType::Down && !event.is_repeat {
695 if let Some(action) = repose_core::shortcuts::resolve_action(
696 repose_core::shortcuts::KeyChord::new(event.key.clone(), self.modifiers),
697 ) {
698 if self.dispatch_action(f, action.clone()) {
699 request_frame();
700 return true;
701 }
702 if let Some(new_id) = repose_core::focus::handle_action(&action, &mut self.sched, f)
704 {
705 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == new_id) {
707 if let Some(key) = hit.tf_state_key {
708 self.textfield_states.entry(key).or_insert_with(|| {
709 Rc::new(RefCell::new(TextFieldState::new()))
710 });
711 }
712 }
713 request_frame();
714 return true;
715 }
716 }
717 }
718
719 if let Some(fid) = self.sched.focused {
721 let is_tf = f.semantics_nodes.iter().any(|n| n.id == fid && n.role == repose_core::semantics::Role::TextField);
722 if !is_tf {
723 if event.event_type == KeyEventType::Down && !event.is_repeat {
724 if event.key == Key::Space || event.key == Key::Enter {
725 self.pressed_ids.insert(fid);
726 self.key_pressed_active = Some(fid);
727 request_frame();
728 return true;
729 }
730 } else if event.event_type == KeyEventType::Up {
731 if let Some(active_id) = self.key_pressed_active {
732 if event.key == Key::Space || event.key == Key::Enter {
733 self.pressed_ids.remove(&active_id);
734 self.key_pressed_active = None;
735 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == active_id) {
736 if let Some(cb) = &hit.on_click {
737 cb();
738 } else if let Some(cb) = &hit.on_pointer_down {
739 let pe = PointerEvent::new(
740 PointerId(0),
741 PointerKind::Mouse,
742 PointerEventKind::Down(PointerButton::Primary),
743 Vec2 { x: 0.0, y: 0.0 },
744 1.0,
745 self.modifiers,
746 );
747 cb(pe);
748 }
749 }
750 request_frame();
751 return true;
752 }
753 }
754 }
755 }
756 }
757
758 if event.event_type == KeyEventType::Down && !event.is_repeat && event.key == Key::Enter {
760 if let Some(fid) = self.sched.focused {
761 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
762 let is_multiline = hit.tf_multiline;
763 let should_submit = if is_multiline {
764 self.modifiers.ctrl || self.modifiers.meta
765 } else {
766 true
767 };
768 if should_submit {
769 if let Some(on_submit) = &hit.on_text_submit {
770 let key = tf_key_of(f, fid);
771 if let Some(state_rc) = self.textfield_states.get(&key) {
772 let text = state_rc.borrow().text.clone();
773 on_submit(text);
774 request_frame();
775 return true;
776 }
777 }
778 } else {
779 let key = tf_key_of(f, fid);
781 if let Some(state_rc) = self.textfield_states.get(&key) {
782 let mut st = state_rc.borrow_mut();
783 st.insert_text("\n");
784 let new_text = st.text.clone();
785 notify_text_change(f, fid, new_text);
786 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
787 request_frame();
788 return true;
789 }
790 }
791 }
792 }
793 }
794
795 if event.event_type == KeyEventType::Down {
797 if let Some(fid) = self.sched.focused {
798 let key = tf_key_of(f, fid);
799 if let Some(state_rc) = self.textfield_states.get(&key) {
800 let mut state = state_rc.borrow_mut();
801 match event.key {
802 Key::Backspace => {
803 state.delete_backward();
804 let new_text = state.text.clone();
805 notify_text_change(f, fid, new_text);
806 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
807 request_frame();
808 return true;
809 }
810 Key::Delete => {
811 state.delete_forward();
812 let new_text = state.text.clone();
813 notify_text_change(f, fid, new_text);
814 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
815 request_frame();
816 return true;
817 }
818 Key::ArrowLeft => {
819 state.move_cursor(-1, self.modifiers.shift);
820 state.preferred_x_px = None;
821 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
822 request_frame();
823 return true;
824 }
825 Key::ArrowRight => {
826 state.move_cursor(1, self.modifiers.shift);
827 state.preferred_x_px = None;
828 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
829 request_frame();
830 return true;
831 }
832 Key::ArrowUp => {
833 if is_multiline_id(f, fid) {
834 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
835 let font_px = dp_to_px(TF_FONT_DP);
836 let cur = state.caret_index();
837 let (new_pos, px) =
838 repose_ui::textfield::move_caret_vertical(
839 &state.text,
840 font_px,
841 hit.rect.w,
842 cur,
843 -1,
844 state.preferred_x_px,
845 );
846 if self.modifiers.shift {
847 state.selection.end = new_pos;
848 } else {
849 state.selection = new_pos..new_pos;
850 }
851 state.preferred_x_px = Some(px);
852 let (cx, cy, _) = caret_xy_for_byte(
853 &state.text,
854 font_px,
855 hit.rect.w,
856 state.caret_index(),
857 );
858 let iw = state.inner_width;
859 let ih = state.inner_height;
860 state.ensure_caret_visible_xy(
861 cx, cy,
862 iw,
863 ih,
864 dp_to_px(2.0),
865 );
866 request_frame();
867 return true;
868 }
869 }
870 }
871 Key::ArrowDown => {
872 if is_multiline_id(f, fid) {
873 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
874 let font_px = dp_to_px(TF_FONT_DP);
875 let cur = state.caret_index();
876 let (new_pos, px) =
877 repose_ui::textfield::move_caret_vertical(
878 &state.text,
879 font_px,
880 hit.rect.w,
881 cur,
882 1,
883 state.preferred_x_px,
884 );
885 if self.modifiers.shift {
886 state.selection.end = new_pos;
887 } else {
888 state.selection = new_pos..new_pos;
889 }
890 state.preferred_x_px = Some(px);
891 let (cx, cy, _) = caret_xy_for_byte(
892 &state.text,
893 font_px,
894 hit.rect.w,
895 state.caret_index(),
896 );
897 let iw = state.inner_width;
898 let ih = state.inner_height;
899 state.ensure_caret_visible_xy(
900 cx, cy,
901 iw,
902 ih,
903 dp_to_px(2.0),
904 );
905 request_frame();
906 return true;
907 }
908 }
909 }
910 Key::Home => {
911 state.selection = 0..0;
912 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
913 request_frame();
914 return true;
915 }
916 Key::End => {
917 let end = state.text.len();
918 state.selection = end..end;
919 tf_ensure_caret_visible(&mut state, is_multiline_id(f, fid));
920 request_frame();
921 return true;
922 }
923 _ => {}
924 }
925 }
926 }
927
928 if !self.ime_preedit
930 && !self.modifiers.ctrl
931 && !self.modifiers.alt
932 && !self.modifiers.meta
933 {
934 if let Key::Character(c) = event.key {
935 if !c.is_control() && c != '\n' && c != '\r' {
936 if let Some(fid) = self.sched.focused {
937 let key = tf_key_of(f, fid);
938 if let Some(state_rc) = self.textfield_states.get(&key) {
939 let mut st = state_rc.borrow_mut();
940 let text = c.to_string();
941 st.insert_text(&text);
942 notify_text_change(f, fid, st.text.clone());
943 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
944 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
945 }
946 request_frame();
947 return true;
948 }
949 }
950 }
951 }
952 }
953 }
954
955 if event.event_type == KeyEventType::Up {
957 if let Some(active_id) = self.key_pressed_active {
958 if event.key == Key::Space || event.key == Key::Enter {
959 self.pressed_ids.remove(&active_id);
960 self.key_pressed_active = None;
961 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == active_id) {
962 if let Some(cb) = &hit.on_click {
963 cb();
964 }
965 }
966 request_frame();
967 return true;
968 }
969 }
970 }
971
972 false
973 }
974
975 fn dispatch_focus_key_event(&self, f: &Frame, event: &KeyEvent) -> bool {
977 let Some(focused) = self.sched.focused else {
978 return false;
979 };
980
981 let hit_by_id: HashMap<u64, &HitRegion> =
982 f.hit_regions.iter().map(|h| (h.id, h)).collect();
983 let sem_parent_of: HashMap<u64, u64> = f
984 .semantics_nodes
985 .iter()
986 .filter_map(|n| n.parent.map(|p| (n.id, p)))
987 .collect();
988
989 let mut ancestors = Vec::new();
991 let mut cur = focused;
992 loop {
993 ancestors.push(cur);
994 if let Some(&p) = sem_parent_of.get(&cur) {
995 cur = p;
996 } else {
997 break;
998 }
999 }
1000
1001 for &id in ancestors.iter().rev() {
1003 if let Some(hit) = hit_by_id.get(&id) {
1004 if let Some(cb) = &hit.on_preview_key_event {
1005 if cb(event.clone()) {
1006 return true;
1007 }
1008 }
1009 }
1010 }
1011
1012 for &id in ancestors.iter() {
1014 if let Some(hit) = hit_by_id.get(&id) {
1015 if let Some(cb) = &hit.on_key_event {
1016 if cb(event.clone()) {
1017 return true;
1018 }
1019 }
1020 }
1021 }
1022
1023 false
1024 }
1025
1026 fn dispatch_action(&self, f: &Frame, action: repose_core::shortcuts::Action) -> bool {
1028 if let Some(fid) = self.sched.focused {
1029 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1030 if let Some(cb) = &hit.on_action {
1031 if cb(action.clone()) {
1032 return true;
1033 }
1034 }
1035 }
1036 }
1037
1038 if repose_core::shortcuts::handle(action.clone()) {
1039 return true;
1040 }
1041
1042 false
1043 }
1044
1045
1046 pub fn handle_ime(&mut self, event: &ImeEvent) {
1048 let Some(fid) = self.sched.focused else {
1049 return;
1050 };
1051 let Some(f) = &self.frame_cache else {
1052 return;
1053 };
1054 let key = tf_key_of(f, fid);
1055 let Some(state_rc) = self.textfield_states.get(&key) else {
1056 return;
1057 };
1058
1059 let mut state = state_rc.borrow_mut();
1060
1061 match event {
1062 ImeEvent::Start => {
1063 self.ime_preedit = false;
1064 }
1065 ImeEvent::Update { text, cursor } => {
1066 state.set_composition(text.clone(), *cursor);
1067 self.ime_preedit = !text.is_empty();
1068 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1069 notify_text_change(f, fid, state.text.clone());
1070 }
1071 ImeEvent::Commit(text) => {
1072 state.commit_composition(text.clone());
1073 self.ime_preedit = false;
1074 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1075 notify_text_change(f, fid, state.text.clone());
1076 }
1077 ImeEvent::Cancel => {
1078 self.ime_preedit = false;
1079 if state.composition.is_some() {
1080 state.cancel_composition();
1081 repose_ui::textfield::ensure_caret_visible(&mut state, true);
1082 notify_text_change(f, fid, state.text.clone());
1083 }
1084 }
1085 }
1086
1087 request_frame();
1088 }
1089
1090
1091 pub fn handle_focus_lost(&mut self) {
1093 dnd::handle_drag_action(&DragAction::Cancel);
1094 self.handle_pointer_cancel();
1095 self.ime_preedit = false;
1096 }
1097
1098
1099 pub fn ensure_textfield_state(&mut self, key: u64) -> Rc<RefCell<TextFieldState>> {
1101 self.textfield_states
1102 .entry(key)
1103 .or_insert_with(|| Rc::new(RefCell::new(TextFieldState::new())))
1104 .clone()
1105 }
1106
1107 pub fn tf_key_of(&self, visual_id: u64) -> u64 {
1109 self.frame_cache
1110 .as_ref()
1111 .map(|f| tf_key_of(f, visual_id))
1112 .unwrap_or(visual_id)
1113 }
1114
1115 pub fn is_textfield(&self, id: u64) -> bool {
1117 self.frame_cache
1118 .as_ref()
1119 .map(|f| is_textfield_in_frame(f, id))
1120 .unwrap_or(false)
1121 }
1122
1123 pub fn is_multiline(&self, id: u64) -> bool {
1125 self.frame_cache
1126 .as_ref()
1127 .map(|f| is_multiline_id(f, id))
1128 .unwrap_or(false)
1129 }
1130
1131
1132 pub fn paste_into_focused(&mut self, text: &str) {
1134 let Some(fid) = self.sched.focused else {
1135 return;
1136 };
1137 let Some(f) = &self.frame_cache.clone() else {
1138 return;
1139 };
1140 let key = tf_key_of(f, fid);
1141 if let Some(state_rc) = self.textfield_states.get(&key) {
1142 let mut st = state_rc.borrow_mut();
1143 st.insert_text_atomic(text);
1144 let new_text = st.text.clone();
1145 notify_text_change(f, fid, new_text);
1146 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid) {
1147 tf_ensure_caret_visible(&mut st, hit.tf_multiline);
1148 }
1149 }
1150 }
1151
1152 pub fn cursor_suggestion(&self) -> Option<CursorIcon> {
1154 self.cursor
1155 }
1156
1157 pub fn take_cursor_suggestion(&mut self) -> Option<CursorIcon> {
1159 self.cursor.take()
1160 }
1161}
1162
1163impl Default for ReposeRuntime {
1164 fn default() -> Self {
1165 Self::new()
1166 }
1167}
1168
1169
1170pub fn compose_frame_inner<F>(
1172 sched: &mut Scheduler,
1173 root_fn: &mut F,
1174 scale: f32,
1175 size_px_u32: (u32, u32),
1176 hover_id: Option<u64>,
1177 pressed_ids: &HashSet<u64>,
1178 tf_states: &HashMap<u64, Rc<RefCell<TextFieldState>>>,
1179) -> Frame
1180where
1181 F: FnMut(&mut Scheduler) -> View,
1182{
1183 if let Some(requested_id) = take_focus_request() {
1184 if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
1185 sched.focused = None;
1186 } else {
1187 sched.focused = Some(requested_id);
1188 }
1189 }
1190
1191 set_density_default(Density { scale });
1192
1193 let current_focused = sched.focused;
1194
1195 let frame = sched.repose(
1196 {
1197 let scale = scale;
1198 move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s))
1199 },
1200 {
1201 let hover_id = hover_id;
1202 let pressed_ids = pressed_ids.clone();
1203 move |view, _size| {
1204 let interactions = Interactions {
1205 hover: hover_id,
1206 pressed: pressed_ids.clone(),
1207 };
1208 with_density(Density { scale }, || {
1209 layout_and_paint(view, size_px_u32, tf_states, &interactions, current_focused)
1210 })
1211 }
1212 },
1213 );
1214
1215 if let Some(fid) = sched.focused {
1216 if !frame.focus_chain.contains(&fid) {
1217 sched.focused = None;
1218 }
1219 }
1220
1221 frame
1222}
1223
1224fn dispatch_hover_change(
1228 frame: Option<&Frame>,
1229 hover_id: &mut Option<u64>,
1230 new_hover: Option<u64>,
1231 pos: Vec2,
1232 modifiers: Modifiers,
1233) {
1234 let Some(f) = frame else {
1235 *hover_id = None;
1236 return;
1237 };
1238 if new_hover == *hover_id {
1239 return;
1240 }
1241 if let Some(prev_id) = *hover_id {
1242 if let Some(prev) = f.hit_regions.iter().find(|h| h.id == prev_id) {
1243 if let Some(cb) = &prev.on_pointer_leave {
1244 let pe = PointerEvent::new(
1245 PointerId(0),
1246 PointerKind::Mouse,
1247 PointerEventKind::Leave,
1248 pos,
1249 1.0,
1250 modifiers,
1251 );
1252 cb(pe);
1253 }
1254 }
1255 }
1256 if let Some(hid) = new_hover {
1257 if let Some(h) = f.hit_regions.iter().find(|h| h.id == hid) {
1258 if let Some(cb) = &h.on_pointer_enter {
1259 let pe = PointerEvent::new(
1260 PointerId(0),
1261 PointerKind::Mouse,
1262 PointerEventKind::Enter,
1263 pos,
1264 1.0,
1265 modifiers,
1266 );
1267 cb(pe);
1268 }
1269 }
1270 }
1271 *hover_id = new_hover;
1272}
1273
1274fn is_textfield_in_frame(f: &Frame, id: u64) -> bool {
1275 f.semantics_nodes
1276 .iter()
1277 .any(|n| n.id == id && n.role == repose_core::semantics::Role::TextField)
1278}
1279
1280fn is_semantics_textfield(f: &Frame, id: u64) -> bool {
1281 f.semantics_nodes
1282 .iter()
1283 .any(|n| n.id == id && n.role == repose_core::semantics::Role::TextField)
1284}
1285
1286fn is_multiline_id(f: &Frame, id: u64) -> bool {
1287 f.hit_regions
1288 .iter()
1289 .find(|h| h.id == id)
1290 .map(|h| h.tf_multiline)
1291 .unwrap_or(false)
1292}
1293
1294fn tf_key_of(frame: &Frame, visual_id: u64) -> u64 {
1295 if let Some(i) = frame.hit_regions.iter().position(|h| h.id == visual_id) {
1296 let hr = &frame.hit_regions[i];
1297 return hr.tf_state_key.unwrap_or(hr.id);
1298 }
1299 visual_id
1300}
1301
1302fn notify_text_change(f: &Frame, id: u64, text: String) {
1303 if let Some(h) = f.hit_regions.iter().find(|h| h.id == id) {
1304 if let Some(cb) = &h.on_text_change {
1305 cb(text);
1306 }
1307 }
1308}
1309
1310fn tf_ensure_caret_visible(state: &mut TextFieldState, is_multiline: bool) {
1311 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
1312 let wrap_width = state.inner_width;
1313
1314 if is_multiline {
1315 let (cx, cy, _) = caret_xy_for_byte(&state.text, font_px, wrap_width, state.caret_index());
1316 state.ensure_caret_visible_xy(cx, cy, state.inner_width, state.inner_height, dp_to_px(2.0));
1317 } else {
1318 let caret_idx = state.caret_index();
1319 let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
1320 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
1321 let tfmd = vt.filter(&annotated);
1322 let off = repose_core::original_offset_to_display(&state.text, tfmd.text.as_str(), caret_idx);
1323 (tfmd.text.text, off)
1324 } else {
1325 (state.text.clone(), caret_idx)
1326 };
1327 let m = measure_text(&display, font_px, TextMeasureConfig::default());
1328 let caret_x_px = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
1329 state.ensure_caret_visible(caret_x_px, wrap_width, dp_to_px(2.0));
1330 }
1331}
1332
1333fn index_for_x_bytes_vt(state: &TextFieldState, font_px: f32, x_px: f32) -> usize {
1334 if let Some(vt) = &state.visual_transformation {
1335 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
1336 let tfmd = vt.filter(&annotated);
1337 let display_idx = repose_ui::textfield::index_for_x_bytes(tfmd.text.as_str(), font_px, x_px, 400, 0);
1338 tfmd.offset_mapping.transformed_to_original(display_idx)
1339 } else {
1340 repose_ui::textfield::index_for_x_bytes(&state.text, font_px, x_px, 400, 0)
1341 }
1342}
1343
1344fn index_for_xy_bytes_vt(
1345 state: &TextFieldState,
1346 font_px: f32,
1347 wrap_w: f32,
1348 x_px: f32,
1349 y_px: f32,
1350) -> usize {
1351 if let Some(vt) = &state.visual_transformation {
1352 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
1353 let tfmd = vt.filter(&annotated);
1354 let display_idx = repose_ui::textfield::index_for_xy_bytes(tfmd.text.as_str(), font_px, wrap_w, x_px, y_px);
1355 tfmd.offset_mapping.transformed_to_original(display_idx)
1356 } else {
1357 repose_ui::textfield::index_for_xy_bytes(&state.text, font_px, wrap_w, x_px, y_px)
1358 }
1359}
1360
1361fn dispatch_scroll(
1363 frame: &Frame,
1364 pos: Vec2,
1365 delta: Vec2,
1366 scroll_capture: Option<u64>,
1367) -> (bool, Option<u64>) {
1368 if let Some(cid) = scroll_capture {
1369 if let Some(cb) = frame
1370 .hit_regions
1371 .iter()
1372 .find(|h| h.id == cid)
1373 .and_then(|h| h.on_scroll.as_ref())
1374 {
1375 cb(delta);
1376 return (true, Some(cid));
1377 }
1378 }
1380
1381 let mut remaining = delta;
1382 for hit in frame
1383 .hit_regions
1384 .iter()
1385 .rev()
1386 .filter(|h| h.rect.contains(pos))
1387 {
1388 if let Some(cb) = &hit.on_scroll {
1389 let before = remaining;
1390 let leftover = cb(before);
1391 let consumed = (before.x - leftover.x).abs() > 0.001
1392 || (before.y - leftover.y).abs() > 0.001;
1393 if consumed {
1394 return (true, Some(hit.id));
1395 }
1396 remaining = leftover;
1397 if remaining.x.abs() <= 0.001 && remaining.y.abs() <= 0.001 {
1398 break;
1399 }
1400 }
1401 }
1402 (false, scroll_capture)
1403}