1use super::{ControllerContext, InputController};
2use crate::event::{InputEvent, KeyCode, KeyEvent, PointerEvent};
3use crate::ActionEnvelope;
4use crate::ActionId;
5use fission_diagnostics::prelude as diag;
6use fission_ir::semantics::InputMask;
7use fission_ir::{
8 op::{self, LayoutOp, Op},
9 NodeId, Semantics,
10};
11use fission_layout::LayoutSnapshot;
12use serde_json;
13use unicode_segmentation::UnicodeSegmentation;
14
15pub struct TextInputController;
16
17impl InputController for TextInputController {
18 fn handle_event(&mut self, ctx: &mut ControllerContext, event: &InputEvent) -> bool {
19 match event {
20 InputEvent::Keyboard(KeyEvent::Down {
21 key_code,
22 modifiers,
23 }) => self.handle_key(ctx, key_code.clone(), *modifiers),
24 InputEvent::Ime(ime) => self.handle_ime(ctx, ime),
25 InputEvent::Pointer(PointerEvent::Down { point, .. }) => {
26 if let Some(focused_id) = ctx.interaction.focused {
27 if let Some(node) = ctx.ir.nodes.get(&focused_id) {
28 if let Op::Semantics(sem) = &node.op {
29 if sem.role == fission_ir::semantics::Role::TextInput {
30 if let Some(geom) = ctx.layout.get_node_geometry(focused_id) {
37 if !geom.rect.contains(*point) {
38 return false;
39 }
40 }
41 if let Some((scroll_id, _text_op_node_id, scroll_direction)) =
42 Self::find_scroll_container_and_text_op(
43 ctx.ir,
44 focused_id,
45 sem.multiline,
46 )
47 {
48 if let Some(scroll_geom) =
49 ctx.layout.get_node_geometry(scroll_id)
50 {
51 let value = sem.value.as_deref().unwrap_or("");
52 let offset = ctx.scroll.get_offset(scroll_id);
53
54 let caret = if let Some(measurer) = ctx.measurer {
55 let font_size = 16.0;
56 let max_width = if sem.multiline
57 && scroll_geom.rect.width() > 0.0
58 {
59 Some(scroll_geom.rect.width())
60 } else {
61 None
62 };
63 measurer.hit_test(
64 value,
65 font_size,
66 max_width,
67 point.x - scroll_geom.rect.origin.x + offset,
68 point.y - scroll_geom.rect.origin.y,
69 )
70 } else {
71 Self::caret_from_point_in_text_fallback(
72 value,
73 16.0,
74 scroll_geom.rect.origin.x,
75 scroll_geom.rect.size.width,
76 scroll_geom.content_size.width,
77 offset,
78 point.x,
79 )
80 };
81 let st = ctx.text_edit.get_mut_or_default(focused_id);
82 st.caret = caret;
83 st.anchor = caret;
84 Self::dispatch_cursor_change(ctx, sem, focused_id, caret, caret);
85 }
86 }
87 return true;
88 }
89 }
90 }
91 }
92 false
93 }
94 InputEvent::Pointer(PointerEvent::Move { point, .. }) => {
95 if let Some(focused_id) = ctx.interaction.focused {
96 if let Some(node) = ctx.ir.nodes.get(&focused_id) {
97 if let Op::Semantics(sem) = &node.op {
98 if sem.role == fission_ir::semantics::Role::TextInput {
99 if !ctx.interaction.pressed.is_empty() {
100 let mut moved_enough = true;
101 if let Some(start) = ctx.interaction.last_down_point {
102 let dx = point.x - start.x;
103 let dy = point.y - start.y;
104 if dx * dx + dy * dy < 4.0 {
105 moved_enough = false;
106 }
107 }
108 if moved_enough {
109 if let Some((
110 scroll_id,
111 _text_op_node_id,
112 scroll_direction,
113 )) = Self::find_scroll_container_and_text_op(
114 ctx.ir,
115 focused_id,
116 sem.multiline,
117 ) {
118 if let Some(scroll_geom) =
119 ctx.layout.get_node_geometry(scroll_id)
120 {
121 let value = sem.value.as_deref().unwrap_or("");
122 let offset = ctx.scroll.get_offset(scroll_id);
123 let new_caret = if let Some(measurer) = ctx.measurer
124 {
125 let font_size = 16.0;
126 let max_width = if sem.multiline
127 && scroll_geom.rect.width() > 0.0
128 {
129 Some(scroll_geom.rect.width())
130 } else {
131 None
132 };
133 measurer.hit_test(
134 value,
135 font_size,
136 max_width,
137 point.x - scroll_geom.rect.origin.x
138 + offset,
139 point.y - scroll_geom.rect.origin.y,
140 )
141 } else {
142 Self::caret_from_point_in_text_fallback(
143 value,
144 16.0,
145 scroll_geom.rect.origin.x,
146 scroll_geom.rect.size.width,
147 scroll_geom.content_size.width,
148 offset,
149 point.x,
150 )
151 };
152 let st =
153 ctx.text_edit.get_mut_or_default(focused_id);
154 st.caret = new_caret;
155 let current_anchor = st.anchor;
156 Self::auto_scroll_textinput(ctx, focused_id);
157 Self::dispatch_cursor_change(ctx, sem, focused_id, new_caret, current_anchor);
158 }
159 }
160 }
161 }
162 return true;
163 }
164 }
165 }
166 }
167 false
168 }
169 _ => false,
170 }
171 }
172}
173
174impl TextInputController {
175 fn handle_key(
176 &mut self,
177 ctx: &mut ControllerContext,
178 key_code: KeyCode,
179 modifiers: u8,
180 ) -> bool {
181 let focused_id = if let Some(id) = ctx.interaction.focused {
182 id
183 } else {
184 return false;
185 };
186
187 let mut semantics_node = None;
188 let mut current_id = Some(focused_id);
189 while let Some(node_id) = current_id {
190 if let Some(node) = ctx.ir.nodes.get(&node_id) {
191 if let Op::Semantics(s) = &node.op {
192 if s.role == fission_ir::semantics::Role::TextInput {
193 semantics_node = Some(s);
194 break;
195 }
196 }
197 current_id = node.parent;
198 } else {
199 break;
200 }
201 }
202
203 let semantics = if let Some(s) = semantics_node {
204 s
205 } else {
206 return false;
207 };
208
209 let (value, mut caret, mut anchor) =
210 Self::resolve_editing_value(ctx, focused_id, semantics.value.as_deref().unwrap_or(""));
211
212 caret = Self::clamp_caret_to_value(&value, caret);
213 anchor = Self::clamp_caret_to_value(&value, anchor);
214
215 let sel = if caret != anchor {
216 Some((anchor, caret))
217 } else {
218 None
219 };
220
221 let mut next_caret = caret;
223 let mut next_anchor = anchor;
224 let mut next_text: Option<String> = None;
225 let mut handled = false;
226
227 let mut undo_redo_result: Option<(String, usize, usize)> = None;
229
230 match key_code {
231 KeyCode::Space => {
232 let (txt, c) = Self::insert_text(&value, caret, sel, " ");
233 next_text = Some(txt);
234 next_caret = c;
235 next_anchor = c;
236 handled = true;
237 }
238 KeyCode::Char(ch) if ((modifiers & 4) != 0) || ((modifiers & 8) != 0) => {
239 let lower = ch.to_ascii_lowercase();
240 let (s, e) = if caret <= anchor {
241 (caret, anchor)
242 } else {
243 (anchor, caret)
244 };
245 match lower {
246 'c' => {
247 if s != e {
248 let txt = value[s..e].to_string();
249 if let Some(cb) = ctx.clipboard {
250 cb.set_text(&txt);
251 }
252 }
253 handled = true;
254 }
255 'x' => {
256 if s != e {
257 let txt = value[s..e].to_string();
258 if let Some(cb) = ctx.clipboard {
259 cb.set_text(&txt);
260 }
261 let mut out = String::with_capacity(value.len() - (e - s));
262 out.push_str(&value[..s]);
263 out.push_str(&value[e..]);
264 next_text = Some(out);
265 next_caret = s;
266 next_anchor = s;
267 }
268 handled = true;
269 }
270 'v' => {
271 let text_to_paste = if let Some(cb) = ctx.clipboard {
272 cb.get_text().unwrap_or_default()
273 } else {
274 String::new()
275 };
276 if !text_to_paste.is_empty() {
277 let (txt, c) = Self::insert_text(&value, caret, sel, &text_to_paste);
278 next_text = Some(txt);
279 next_caret = c;
280 next_anchor = c;
281 }
282 handled = true;
283 }
284 'z' => {
285 let (ctrl_or_super, shift) = (
286 ((modifiers & 4) != 0) || ((modifiers & 8) != 0),
287 (modifiers & 1) != 0,
288 );
289 if ctrl_or_super {
290 let st = ctx.text_edit.get_mut_or_default(focused_id);
291 if shift {
292 if let Some((v, c, a)) = st.history.redo() {
293 undo_redo_result = Some((v.clone(), *c, *a));
294 }
295 } else {
296 if let Some((v, c, a)) = st.history.undo() {
297 undo_redo_result = Some((v.clone(), *c, *a));
298 }
299 }
300 handled = true;
301 }
302 }
303 _ => {} }
305 }
306 KeyCode::Char(c) => {
307 if let Some(mask) = &semantics.input_mask {
309 if !mask.is_valid_char(c) {
310 return true; }
312 }
313 let (txt, nc) = Self::insert_text(&value, caret, sel, &c.to_string());
314 next_text = Some(txt);
315 next_caret = nc;
316 next_anchor = nc;
317 handled = true;
318 }
319 KeyCode::Backspace => {
320 let (txt, nc) = if (modifiers & 2) != 0 && sel.is_none() {
321 let mut at = caret;
323 while at > 0 {
324 let prev = Self::prev_grapheme_boundary(&value, at);
325 let ch = value[prev..].chars().next().unwrap_or('\0');
326 if !ch.is_whitespace() {
327 at = prev;
328 break;
329 }
330 at = prev;
331 }
332 while at > 0 {
333 let prev = Self::prev_grapheme_boundary(&value, at);
334 let ch = value[prev..].chars().next().unwrap_or('\0');
335 if ch.is_alphanumeric() || ch == '_' {
336 at = prev;
337 } else {
338 break;
339 }
340 }
341 let mut out = String::with_capacity(value.len() - (caret - at));
342 out.push_str(&value[..at]);
343 out.push_str(&value[caret..]);
344 (out, at)
345 } else {
346 Self::delete_prev_grapheme(&value, caret, sel)
347 };
348 next_text = Some(txt);
349 next_caret = nc;
350 next_anchor = nc;
351 handled = true;
352 }
353 KeyCode::Left => {
354 let prev = if (modifiers & 2) != 0 {
355 Self::prev_word_boundary(&value, caret)
357 } else {
358 Self::prev_grapheme_boundary(&value, caret)
359 };
360 next_caret = prev;
361 if (modifiers & 1) != 0 {
362 next_anchor = anchor;
363 } else {
364 next_anchor = prev;
365 }
366 handled = true;
367 }
368 KeyCode::Right => {
369 let next = if (modifiers & 2) != 0 {
370 Self::next_word_boundary(&value, caret)
372 } else {
373 Self::next_grapheme_boundary(&value, caret)
374 };
375 next_caret = next;
376 if (modifiers & 1) != 0 {
377 next_anchor = anchor;
378 } else {
379 next_anchor = next;
380 }
381 handled = true;
382 }
383 KeyCode::Home => {
384 next_caret = 0;
385 if (modifiers & 1) != 0 {
386 next_anchor = anchor;
387 } else {
388 next_anchor = 0;
389 }
390 handled = true;
391 }
392 KeyCode::End => {
393 let end = value.len();
394 next_caret = end;
395 if (modifiers & 1) != 0 {
396 next_anchor = anchor;
397 } else {
398 next_anchor = end;
399 }
400 handled = true;
401 }
402 KeyCode::Enter => {
403 if semantics.multiline {
404 let insert_str = if semantics.auto_indent {
405 let line_start = value[..caret].rfind('\n').map(|p| p + 1).unwrap_or(0);
407 let leading: String = value[line_start..]
408 .chars()
409 .take_while(|c| *c == ' ' || *c == '\t')
410 .collect();
411 format!("\n{}", leading)
412 } else {
413 "\n".to_string()
414 };
415 let (txt, nc) = Self::insert_text(&value, caret, sel, &insert_str);
416 next_text = Some(txt);
417 next_caret = nc;
418 next_anchor = nc;
419 handled = true;
420 }
421 }
422 KeyCode::Up => {
423 if semantics.multiline {
424 self.handle_vertical_navigation(
425 ctx, focused_id, semantics, &value, caret, modifiers, true,
426 );
427 return true; }
429 }
430 KeyCode::Down => {
431 if semantics.multiline {
432 self.handle_vertical_navigation(
433 ctx, focused_id, semantics, &value, caret, modifiers, false,
434 );
435 return true;
436 }
437 }
438 KeyCode::Tab => {
439 if semantics.capture_tab {
440 let tab_str = " "; let (txt, nc) = Self::insert_text(&value, caret, sel, tab_str);
442 next_text = Some(txt);
443 next_caret = nc;
444 next_anchor = nc;
445 handled = true;
446 }
447 }
450 _ => {} }
452
453 if let Some((v, c, a)) = undo_redo_result {
454 let st = ctx.text_edit.get_mut_or_default(focused_id);
456 st.caret = c;
457 st.anchor = a;
458 st.last_value = v.clone();
459 self.dispatch_change(ctx, semantics, focused_id, v, c);
460 Self::dispatch_cursor_change(ctx, semantics, focused_id, c, a);
461 return true;
462 }
463
464 if let Some(txt) = next_text {
465 let st = ctx.text_edit.get_mut_or_default(focused_id);
467 st.caret = next_caret;
468 st.anchor = next_anchor;
469 st.history.push(txt.clone(), next_caret, next_anchor);
470 st.last_value = txt.clone();
471
472 self.dispatch_change(ctx, semantics, focused_id, txt, next_caret);
473 Self::dispatch_cursor_change(ctx, semantics, focused_id, next_caret, next_anchor);
474 } else if handled {
475 let st = ctx.text_edit.get_mut_or_default(focused_id);
477 st.caret = next_caret;
478 st.anchor = next_anchor;
479 Self::auto_scroll_textinput(ctx, focused_id);
480 Self::dispatch_cursor_change(ctx, semantics, focused_id, next_caret, next_anchor);
481 }
482
483 handled
484 }
485
486 fn handle_ime(&mut self, ctx: &mut ControllerContext, ime: &crate::event::ImeEvent) -> bool {
487 match ime {
488 crate::event::ImeEvent::Commit { text } => {
489 if let Some(focused_id) = ctx.interaction.focused {
490 if let Some(node) = ctx.ir.nodes.get(&focused_id) {
491 if let Op::Semantics(semantics) = &node.op {
492 if semantics.role == fission_ir::semantics::Role::TextInput {
493 let (value, caret, anchor) = Self::resolve_editing_value(
494 ctx,
495 focused_id,
496 semantics.value.as_deref().unwrap_or(""),
497 );
498 let st = ctx.text_edit.get_mut_or_default(focused_id);
499 let caret = Self::clamp_caret_to_value(&value, caret);
500 let sel = if caret != anchor {
501 Some((anchor, caret))
502 } else {
503 None
504 };
505
506 let mut filtered_text = String::new();
507 if let Some(mask) = &semantics.input_mask {
508 for ch in text.chars() {
509 if mask.is_valid_char(ch) {
510 filtered_text.push(ch);
511 }
512 }
513 } else {
514 filtered_text = text.clone();
515 }
516
517 if !filtered_text.is_empty() {
518 let (new_text, new_caret) =
520 Self::insert_text(&value, caret, sel, &filtered_text);
521 st.caret = new_caret;
522 st.anchor = new_caret;
523 st.last_value = new_text.clone();
524 st.history.push(new_text.clone(), new_caret, new_caret);
525 self.dispatch_change(
526 ctx, semantics, focused_id, new_text, new_caret,
527 );
528 }
529
530 *ctx.ime_preedit = None;
531 return true;
532 }
533 }
534 }
535 }
536 }
537 crate::event::ImeEvent::Preedit { text } => {
538 if let Some(focused_id) = ctx.interaction.focused {
539 *ctx.ime_preedit = Some((focused_id, text.clone()));
540 Self::auto_scroll_textinput(ctx, focused_id);
541 return true;
542 }
543 }
544 }
545 false
546 }
547
548 fn dispatch_change(
549 &self,
550 ctx: &mut ControllerContext,
551 semantics: &fission_ir::Semantics,
552 node_id: NodeId,
553 new_text: String,
554 new_caret: usize,
555 ) {
556 if let Some(st) = ctx.text_edit.states.get_mut(&node_id) {
557 st.last_value = new_text.clone();
558 st.pending_model_sync = true;
559 }
560
561 if let Some(action_entry) = semantics.actions.entries.iter().find(|e| {
562 e.trigger == fission_ir::semantics::ActionTrigger::Change
563 }) {
564 let payload = serde_json::to_vec(&new_text).unwrap();
565 let envelope = ActionEnvelope {
566 id: ActionId::from_u128(action_entry.action_id),
567 payload,
568 };
569 ctx.dispatched_actions
570 .push((node_id, envelope, crate::ActionInput::None));
571
572 Self::auto_scroll_textinput(ctx, node_id);
575 }
576 }
577
578 fn dispatch_cursor_change(
579 ctx: &mut ControllerContext,
580 semantics: &fission_ir::Semantics,
581 node_id: NodeId,
582 new_caret: usize,
583 new_anchor: usize,
584 ) {
585 if let Some(st) = ctx.text_edit.states.get(&node_id) {
589 if st.last_dispatched_cursor == Some((new_caret, new_anchor)) {
590 return;
591 }
592 }
593
594 if let Some(action_entry) = semantics.actions.entries.iter().find(|e| {
595 e.trigger == fission_ir::semantics::ActionTrigger::CursorChange
596 }) {
597 if let Some(st) = ctx.text_edit.states.get_mut(&node_id) {
599 st.last_dispatched_cursor = Some((new_caret, new_anchor));
600 }
601
602 let cursor_changed = crate::action::CursorChanged {
603 caret: new_caret,
604 anchor: new_anchor,
605 };
606 let payload = serde_json::to_vec(&cursor_changed).unwrap();
607 let envelope = ActionEnvelope {
608 id: ActionId::from_u128(action_entry.action_id),
609 payload,
610 };
611 ctx.dispatched_actions
612 .push((node_id, envelope, crate::ActionInput::None));
613 }
614 }
615
616 fn resolve_editing_value(
617 ctx: &mut ControllerContext,
618 focused_id: NodeId,
619 semantic_value: &str,
620 ) -> (String, usize, usize) {
621 let st = ctx.text_edit.get_mut_or_default(focused_id);
622
623 if st.pending_model_sync && st.last_value == semantic_value {
626 st.pending_model_sync = false;
627 }
628
629 if !st.pending_model_sync && st.last_value != semantic_value {
632 st.last_value = semantic_value.to_string();
633 st.caret = st.caret.min(st.last_value.len());
634 st.anchor = st.anchor.min(st.last_value.len());
635 st.history.push(st.last_value.clone(), st.caret, st.anchor);
636 }
637
638 let value = if st.pending_model_sync {
639 st.last_value.clone()
640 } else {
641 semantic_value.to_string()
642 };
643
644 if st.history.stack.is_empty() {
645 st.history.push(value.clone(), st.caret, st.anchor);
646 }
647
648 (value, st.caret, st.anchor)
649 }
650
651 fn clamp_caret_to_value(value: &str, caret: usize) -> usize {
652 if caret > value.len() {
653 value.len()
654 } else {
655 caret
656 }
657 }
658
659 fn prev_grapheme_boundary(value: &str, idx: usize) -> usize {
660 let mut last = 0;
661 for (pos, _) in value.grapheme_indices(true) {
662 if pos >= idx {
663 break;
664 }
665 last = pos;
666 }
667 last
668 }
669
670 fn next_grapheme_boundary(value: &str, idx: usize) -> usize {
671 for (pos, _) in value.grapheme_indices(true) {
672 if pos > idx {
673 return pos;
674 }
675 }
676 value.len()
677 }
678
679 fn prev_word_boundary(value: &str, idx: usize) -> usize {
680 let mut at = idx.min(value.len());
681 while at > 0 {
682 let prev = Self::prev_grapheme_boundary(value, at);
683 let ch = value[prev..].chars().next().unwrap_or('\0');
684 if !ch.is_whitespace() {
685 at = prev;
686 break;
687 }
688 at = prev;
689 }
690 while at > 0 {
691 let prev = Self::prev_grapheme_boundary(value, at);
692 let ch = value[prev..].chars().next().unwrap_or('\0');
693 if ch.is_alphanumeric() || ch == '_' {
694 at = prev;
695 } else {
696 break;
697 }
698 }
699 at
700 }
701
702 fn next_word_boundary(value: &str, idx: usize) -> usize {
703 let mut at = idx.min(value.len());
704 while at < value.len() {
705 let next = Self::next_grapheme_boundary(value, at);
706 let ch = value[at..].chars().next().unwrap_or('\0');
707 if !ch.is_whitespace() {
708 at = next;
709 break;
710 }
711 at = next;
712 }
713 while at < value.len() {
714 let next = Self::next_grapheme_boundary(value, at);
715 let ch = value[at..].chars().next().unwrap_or('\0');
716 if ch.is_alphanumeric() || ch == '_' {
717 at = next;
718 } else {
719 break;
720 }
721 }
722 at
723 }
724
725 fn delete_prev_grapheme(
726 value: &str,
727 caret: usize,
728 sel: Option<(usize, usize)>,
729 ) -> (String, usize) {
730 if let Some((a, b)) = sel {
731 let (s, e) = if a <= b { (a, b) } else { (b, a) };
732 let mut out = String::with_capacity(value.len() - (e - s));
733 out.push_str(&value[..s]);
734 out.push_str(&value[e..]);
735 return (out, s);
736 }
737 let at = caret.min(value.len());
738 if at == 0 {
739 return (value.to_string(), 0);
740 }
741 let prev = Self::prev_grapheme_boundary(value, at);
742 let mut out = String::with_capacity(value.len() - (at - prev));
743 out.push_str(&value[..prev]);
744 out.push_str(&value[at..]);
745 (out, prev)
746 }
747
748 fn insert_text(
749 value: &str,
750 caret: usize,
751 sel: Option<(usize, usize)>,
752 text: &str,
753 ) -> (String, usize) {
754 let (s, e) = sel
755 .map(|(a, b)| if a <= b { (a, b) } else { (b, a) })
756 .unwrap_or((caret, caret));
757 let mut out = String::with_capacity(value.len() - (e - s) + text.len());
758 out.push_str(&value[..s]);
759 out.push_str(text);
760 out.push_str(&value[e..]);
761 (out, s + text.len())
762 }
763
764 fn find_scroll_container_and_text_op(
765 ir: &fission_ir::CoreIR,
766 root: NodeId,
767 multiline_semantics: bool,
768 ) -> Option<(NodeId, NodeId, op::FlexDirection)> {
769 let mut stack = vec![root];
770 while let Some(id) = stack.pop() {
771 if let Some(n) = ir.nodes.get(&id) {
772 if let Op::Layout(op::LayoutOp::Scroll { direction, .. }) = &n.op {
773 let matches_multiline_config = (multiline_semantics
774 && *direction == op::FlexDirection::Column)
775 || (!multiline_semantics && *direction == op::FlexDirection::Row);
776 if matches_multiline_config {
777 let mut q = vec![id]; while let Some(cid) = q.pop() {
779 if let Some(cn) = ir.nodes.get(&cid) {
780 if matches!(
781 cn.op,
782 Op::Paint(fission_ir::PaintOp::DrawText { .. })
783 | Op::Paint(fission_ir::PaintOp::DrawRichText { .. })
784 ) {
785 return Some((id, cid, *direction));
786 }
787 for &gc in &cn.children {
788 q.push(gc);
789 }
790 }
791 }
792 return None; }
794 }
795 for &c in &n.children {
796 stack.push(c);
797 }
798 }
799 }
800 None
801 }
802
803 fn find_caret_in_scroll(ir: &fission_ir::CoreIR, scroll_id: NodeId) -> Option<NodeId> {
804 let mut q = vec![scroll_id];
805 while let Some(id) = q.pop() {
806 if let Some(n) = ir.nodes.get(&id) {
807 if let Op::Layout(op::LayoutOp::Box { width: Some(w), .. }) = &n.op {
808 if (*w - 2.0).abs() < 0.01 {
809 let mut has_paint = false;
810 for &cid in &n.children {
811 if let Some(cn) = ir.nodes.get(&cid) {
812 if let Op::Paint(fission_ir::PaintOp::DrawRect { .. }) = cn.op {
813 has_paint = true;
814 break;
815 }
816 }
817 }
818 if has_paint {
819 return Some(id);
820 }
821 }
822 }
823 for &c in &n.children {
824 q.push(c);
825 }
826 }
827 }
828 None
829 }
830
831 fn caret_from_point_in_text_fallback(
832 value: &str,
833 font_size: f32,
834 viewport_x: f32,
835 viewport_w: f32,
836 content_w: f32,
837 scroll_offset: f32,
838 point_x: f32,
839 ) -> usize {
840 0
843 }
844
845 fn auto_scroll_textinput(ctx: &mut ControllerContext, text_root: NodeId) {
846 let font_size = 16.0; if let Some(measurer) = ctx.measurer {
848 let is_multiline = if let Some(node) = ctx.ir.nodes.get(&text_root) {
850 if let Op::Semantics(sem) = &node.op {
851 sem.multiline
852 } else {
853 false
854 }
855 } else {
856 false
857 };
858
859 if let Some((scroll_id, _text_op_node_id, scroll_direction)) =
860 Self::find_scroll_container_and_text_op(ctx.ir, text_root, is_multiline)
861 {
862 if let Some(scroll_geom) = ctx.layout.get_node_geometry(scroll_id) {
863 let viewport_size = scroll_geom.rect.size;
864
865 let current_text_value = if let Some(node) = ctx.ir.nodes.get(&text_root) {
866 if let Op::Semantics(sem) = &node.op {
867 sem.value.clone().unwrap_or_default()
868 } else {
869 String::new()
870 }
871 } else {
872 String::new()
873 };
874
875 let current_caret_idx = if let Some(st) = ctx.text_edit.get(text_root) {
876 st.caret
877 } else {
878 0
879 };
880 let measurer_width = if scroll_direction == op::FlexDirection::Column {
881 Some(viewport_size.width)
882 } else {
883 None
884 };
885
886 let (caret_x, caret_y) = measurer.get_caret_position(
887 ¤t_text_value,
888 font_size,
889 measurer_width,
890 current_caret_idx,
891 );
892
893 let mut offset = ctx.scroll.get_offset(scroll_id);
894
895 if scroll_direction == op::FlexDirection::Row {
896 let caret_left = caret_x;
898 let caret_width = 2.0f32;
899 let caret_right = caret_left + caret_width;
900
901 let margin_left = 2.0f32;
902 let margin_right = 3.0f32;
903
904 let visible_left = caret_left - offset;
905 let visible_right = caret_right - offset;
906
907 if visible_right > (viewport_size.width - margin_right) {
908 offset =
909 (caret_right - (viewport_size.width - margin_right)).max(0.0f32);
910 } else if visible_left < margin_left {
911 offset = (caret_left - margin_left).max(0.0f32);
912 }
913 let content_w = scroll_geom.content_size.width.max(viewport_size.width);
914 let max_offset = (content_w - viewport_size.width).max(0.0f32);
915 offset = offset.clamp(0.0f32, max_offset);
916 ctx.scroll.set_offset(scroll_id, offset);
917 } else {
918 let caret_top = caret_y;
921 let caret_height = measurer
922 .measure("Tg", font_size, Some(viewport_size.width))
923 .1;
924 let caret_bottom = caret_top + caret_height;
925
926 let margin_top = 2.0f32;
927 let margin_bottom = 3.0f32;
928
929 let visible_top = caret_top - offset;
930 let visible_bottom = caret_bottom - offset;
931
932 if visible_bottom > (viewport_size.height - margin_bottom) {
933 offset =
934 (caret_bottom - (viewport_size.height - margin_bottom)).max(0.0f32);
935 } else if visible_top < margin_top {
936 offset = (caret_top - margin_top).max(0.0f32);
937 }
938 let content_h = scroll_geom.content_size.height.max(viewport_size.height);
939 let max_offset = (content_h - viewport_size.height).max(0.0f32);
940 offset = offset.clamp(0.0f32, max_offset);
941 ctx.scroll.set_offset(scroll_id, offset);
942 }
943 }
944 }
945 }
946 }
947
948 fn handle_vertical_navigation(
949 &mut self,
950 ctx: &mut ControllerContext,
951 focused_id: NodeId,
952 semantics: &Semantics,
953 value: &str,
954 caret: usize,
955 modifiers: u8,
956 is_up: bool,
957 ) {
958 if let Some(measurer) = ctx.measurer {
959 if let Some((scroll_id, _text_op_node_id, scroll_direction)) =
960 Self::find_scroll_container_and_text_op(ctx.ir, focused_id, semantics.multiline)
961 {
962 if let Some(scroll_geom) = ctx.layout.get_node_geometry(scroll_id) {
963 let viewport_w = scroll_geom.rect.size.width;
964 let font_size = 16.0;
965
966 let (current_caret_x, current_caret_y) =
967 measurer.get_caret_position(value, font_size, Some(viewport_w), caret);
968
969 let line_metrics =
970 measurer.get_line_metrics(value, font_size, Some(viewport_w));
971
972 let mut current_line_idx = 0;
973 for (idx, line) in line_metrics.iter().enumerate() {
974 if caret >= line.start_index && caret <= line.end_index {
975 current_line_idx = idx;
976 break;
977 }
978 }
979
980 let target_line_idx = if is_up {
981 current_line_idx.saturating_sub(1)
982 } else {
983 (current_line_idx + 1).min(line_metrics.len().saturating_sub(1))
984 };
985
986 if let Some(target_line) = line_metrics.get(target_line_idx) {
987 let target_y = target_line.baseline;
988
989 let mut new_caret_pos = measurer.hit_test(
990 value,
991 font_size,
992 Some(viewport_w),
993 current_caret_x,
994 target_y,
995 );
996
997 new_caret_pos = new_caret_pos.max(target_line.start_index).min(target_line.end_index);
999
1000 let st = ctx.text_edit.get_mut_or_default(focused_id);
1001 st.caret = new_caret_pos;
1002 if (modifiers & 1) == 0 {
1003 st.anchor = new_caret_pos;
1004 } let final_anchor = st.anchor;
1006 Self::auto_scroll_textinput(ctx, focused_id);
1007 Self::dispatch_cursor_change(ctx, semantics, focused_id, new_caret_pos, final_anchor);
1008 }
1009 }
1010 }
1011 }
1012 }
1013}
1014
1015pub fn caret_from_point_in_text(
1018 measurer: Option<&std::sync::Arc<dyn fission_layout::TextMeasurer>>,
1019 value: &str,
1020 font_size: f32,
1021 viewport_x: f32,
1022 viewport_w: f32,
1023 content_w: f32,
1024 scroll_offset: f32,
1025 point_x: f32,
1026) -> usize {
1027 let local_x = (point_x - viewport_x) + scroll_offset;
1028 if local_x <= 0.0 {
1029 return 0;
1030 }
1031 let max_x = content_w.max(viewport_w);
1032 if local_x >= max_x {
1033 return value.len();
1034 }
1035
1036 if let Some(measurer) = measurer {
1037 measurer.hit_test(value, font_size, None, local_x, 0.0)
1040 } else {
1041 TextInputController::caret_from_point_in_text_fallback(
1042 value,
1043 font_size,
1044 viewport_x,
1045 viewport_w,
1046 content_w,
1047 scroll_offset,
1048 point_x,
1049 )
1050 }
1051}