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