1use super::{ControllerContext, InputController};
2use crate::env::TextSelectionHandleKind;
3use crate::event::{
4 EditingCommand, InputEvent, KeyCode, KeyEvent, PointerEvent, MOD_ALT, MOD_CTRL, MOD_SHIFT,
5 MOD_SUPER,
6};
7use crate::ui::widgets::context_menu::TextContextMenuAction;
8use crate::ui::widgets::text_input::{
9 downcast_text_input_runtime_config, text_input_selection_handle_id,
10 text_input_toolbar_button_id, DragStartBehavior, TextScrollPolicy,
11};
12use crate::ActionEnvelope;
13use crate::ActionId;
14use fission_ir::FlexDirection;
15use fission_ir::{
16 op::{self, decode_text_paragraph_style, LayoutOp, Op, TextAlign, TextParagraphStyle},
17 semantics::InputFormatter,
18 Semantics, WidgetId,
19};
20use serde_json;
21use unicode_segmentation::UnicodeSegmentation;
22
23pub struct TextInputController;
24
25impl InputController for TextInputController {
26 fn handle_event(&mut self, ctx: &mut ControllerContext, event: &InputEvent) -> bool {
27 match event {
28 InputEvent::Keyboard(KeyEvent::Down {
29 key_code,
30 modifiers,
31 }) => self.handle_key(ctx, key_code.clone(), *modifiers),
32 InputEvent::Keyboard(KeyEvent::DownWithText {
33 key_code,
34 modifiers,
35 text,
36 }) => self.handle_key_with_produced_text(ctx, key_code.clone(), *modifiers, text),
37 InputEvent::Editing(command) => self.handle_editing_command(ctx, command),
38 InputEvent::TextEdit(command) => self.handle_text_edit_command(ctx, command.clone()),
39 InputEvent::Ime(ime) => self.handle_ime(ctx, ime),
40 InputEvent::Pointer(PointerEvent::Down {
41 point,
42 button,
43 modifiers,
44 ..
45 }) => {
46 let hit = crate::hit_test::hit_test_with_viewports(
47 ctx.ir,
48 ctx.layout,
49 ctx.scroll,
50 ctx.viewport,
51 *point,
52 );
53
54 if let Some(focused_id) = ctx.interaction.focused {
55 if let Some(node) = ctx.ir.nodes.get(&focused_id) {
56 if let Op::Semantics(sem) = &node.op {
57 if sem.role == fission_ir::semantics::Role::TextInput {
58 if let Some(hit_node_id) = hit {
59 if let Some(action) =
60 Self::toolbar_action_hit(ctx.ir, focused_id, hit_node_id)
61 {
62 return self.execute_toolbar_action(ctx, action);
63 }
64 if let Some(handle_kind) =
65 Self::selection_handle_hit(ctx.ir, focused_id, hit_node_id)
66 {
67 let value = sem.value.as_deref().unwrap_or("").to_string();
68 if matches!(button, crate::event::PointerButton::Primary) {
69 ctx.interaction.pressed.clear();
70 ctx.interaction.set_pressed(focused_id, true);
71 ctx.interaction.last_down_point = Some(*point);
72 }
73 let state = ctx.text_edit.get_mut_or_default(focused_id);
74 state.affordances.active_handle = Some(handle_kind);
75 state.affordances.toolbar_visible = false;
76 Self::sync_text_input_affordances(
77 ctx, focused_id, sem, &value, false, None,
78 );
79 return true;
80 }
81 }
82
83 if matches!(button, crate::event::PointerButton::Secondary) {
84 let value = sem.value.as_deref().unwrap_or("").to_string();
85 let wrapper_anchor =
86 Self::input_wrapper_geometry(ctx, focused_id).map(|geom| {
87 fission_layout::LayoutPoint::new(
88 (point.x - geom.rect.origin.x).max(0.0),
89 (point.y - geom.rect.origin.y).max(0.0),
90 )
91 });
92 Self::sync_text_input_affordances(
93 ctx,
94 focused_id,
95 sem,
96 &value,
97 true,
98 wrapper_anchor,
99 );
100 return true;
101 }
102 }
103 }
104 }
105 }
106
107 let effective_focused = if let Some(focused_id) = ctx.interaction.focused {
112 let mut walk = hit;
113 let mut belongs_to_focused = false;
114 while let Some(nid) = walk {
115 if nid == focused_id {
116 belongs_to_focused = true;
117 break;
118 }
119 walk = ctx.ir.nodes.get(&nid).and_then(|n| n.parent);
120 }
121 if belongs_to_focused {
122 Some(focused_id)
123 } else {
124 if let Some(node) = ctx.ir.nodes.get(&focused_id) {
125 if let Op::Semantics(sem) = &node.op {
126 if sem.role == fission_ir::semantics::Role::TextInput {
127 let current_value = sem.value.as_deref().unwrap_or("");
128 let _ = Self::dispatch_action_for_trigger(
129 ctx,
130 sem,
131 focused_id,
132 fission_ir::semantics::ActionTrigger::TapOutside,
133 Some(
134 serde_json::to_vec(¤t_value.to_string()).unwrap(),
135 ),
136 );
137 }
138 }
139 }
140 Self::clear_text_input_affordances(ctx, focused_id);
141 None
142 }
143 } else {
144 hit.and_then(|hit| {
147 let mut walk = Some(hit);
148 while let Some(nid) = walk {
149 if let Some(node) = ctx.ir.nodes.get(&nid) {
150 if let Op::Semantics(s) = &node.op {
151 if s.focusable
152 && s.role == fission_ir::semantics::Role::TextInput
153 {
154 let semantic_value =
155 s.value.as_deref().unwrap_or_default().to_string();
156 let select_all = Self::runtime_config(ctx, nid)
157 .is_some_and(|config| config.select_all_on_focus);
158 ctx.interaction.set_focused(Some(nid));
159 if select_all {
160 Self::sync_runtime_state(
161 ctx,
162 nid,
163 semantic_value.as_str(),
164 );
165 let state = ctx.text_edit.get_mut_or_default(nid);
166 state.anchor = 0;
167 state.caret = state.buffer.len_bytes();
168 }
169 let value = ctx
170 .text_edit
171 .get(nid)
172 .map(|state| state.editing_value())
173 .unwrap_or_else(|| {
174 crate::TextEditingValue::from_text(
175 semantic_value.clone(),
176 )
177 });
178 if let Some((envelope, input)) =
179 crate::input::prepare_scoped_text_session_action(
180 ctx.ir,
181 s,
182 nid,
183 fission_ir::semantics::ActionTrigger::Focus,
184 value,
185 crate::TextEditSource::Pointer,
186 crate::TextEditPhase::Focused,
187 )
188 {
189 ctx.dispatched_actions.push((nid, envelope, input));
190 }
191 return Some(nid);
192 }
193 }
194 walk = node.parent;
195 } else {
196 break;
197 }
198 }
199 None
200 })
201 };
202 if let Some(focused_id) = effective_focused {
203 if let Some(node) = ctx.ir.nodes.get(&focused_id) {
204 if let Op::Semantics(sem) = &node.op {
205 if sem.role == fission_ir::semantics::Role::TextInput {
206 let geom_id = std::iter::successors(Some(focused_id), |id| {
220 ctx.ir
221 .nodes
222 .get(id)
223 .and_then(|n| n.children.first().copied())
224 })
225 .find(|id| ctx.layout.get_node_geometry(*id).is_some())
226 .or_else(|| {
227 let mut w =
228 ctx.ir.nodes.get(&focused_id).and_then(|n| n.parent);
229 while let Some(pid) = w {
230 if ctx.layout.get_node_geometry(pid).is_some() {
231 return Some(pid);
232 }
233 w = ctx.ir.nodes.get(&pid).and_then(|n| n.parent);
234 }
235 None
236 });
237 if let Some(geom) =
238 geom_id.and_then(|id| ctx.layout.get_node_geometry(id))
239 {
240 let mut scroll_adj_y = 0.0f32;
241 let mut scroll_adj_x = 0.0f32;
242 let mut walk_id =
243 ctx.ir.nodes.get(&focused_id).and_then(|n| n.parent);
244 while let Some(pid) = walk_id {
245 if let Some(pnode) = ctx.ir.nodes.get(&pid) {
246 if let Op::Layout(LayoutOp::Scroll {
247 direction, ..
248 }) = &pnode.op
249 {
250 let poff = ctx.scroll.get_offset(pid);
251 match direction {
252 FlexDirection::Row => scroll_adj_x += poff,
253 FlexDirection::Column => scroll_adj_y += poff,
254 }
255 }
256 walk_id = pnode.parent;
257 } else {
258 break;
259 }
260 }
261 let visual_rect = fission_layout::LayoutRect::new(
262 geom.rect.origin.x - scroll_adj_x,
263 geom.rect.origin.y - scroll_adj_y,
264 geom.rect.size.width,
265 geom.rect.size.height,
266 );
267 let _ = visual_rect;
270 }
271 let scroll_result = Self::find_scroll_container_and_text_op(
272 ctx.ir,
273 focused_id,
274 sem.multiline,
275 );
276 if let Some((scroll_id, text_op_node_id, scroll_direction)) =
277 scroll_result
278 {
279 if let Some(scroll_geom) =
280 ctx.layout.get_node_geometry(scroll_id)
281 {
282 if matches!(button, crate::event::PointerButton::Primary) {
283 ctx.interaction.pressed.clear();
284 ctx.interaction.set_pressed(focused_id, true);
285 ctx.interaction.last_down_point = Some(*point);
286 }
287 let value = sem.value.as_deref().unwrap_or("");
288 let display_value =
289 Self::display_value_for_metrics(ctx, focused_id, value);
290 let metric_text = if sem.masked {
291 Self::mask_text_for_metrics(&display_value)
292 } else {
293 display_value.clone()
294 };
295
296 let caret = if let Some(measurer) = ctx.measurer {
297 let local_point = Self::text_local_point_from_screen(
298 ctx,
299 scroll_id,
300 scroll_direction,
301 scroll_geom,
302 *point,
303 );
304
305 let masked_caret = Self::hit_test_text(
306 measurer,
307 ctx.layout,
308 ctx.ir,
309 focused_id,
310 text_op_node_id,
311 sem.masked,
312 &metric_text,
313 scroll_geom,
314 local_point.x,
315 local_point.y,
316 );
317 if sem.masked {
318 Self::source_byte_offset_from_masked(
319 &display_value,
320 &metric_text,
321 masked_caret,
322 )
323 } else {
324 masked_caret
325 }
326 } else {
327 let font_size =
328 Self::extract_font_size(ctx.ir, focused_id)
329 .unwrap_or(13.0);
330 Self::caret_from_point_in_text_fallback(
331 &display_value,
332 font_size,
333 scroll_geom.rect.origin.x,
334 scroll_geom.rect.size.width,
335 scroll_geom.content_size.width,
336 ctx.scroll.get_offset(scroll_id),
337 point.x,
338 )
339 };
340 let anchor = {
341 let st = ctx.text_edit.get_mut_or_default(focused_id);
342 st.caret = caret;
343 if !Self::has_shift(*modifiers) {
344 st.anchor = caret;
345 }
346 st.anchor
347 };
348 Self::dispatch_cursor_change(
349 ctx, sem, focused_id, caret, anchor,
350 );
351 Self::sync_text_input_affordances(
352 ctx, focused_id, sem, value, false, None,
353 );
354 }
355 }
356 return true;
357 }
358 }
359 }
360 }
361
362 false
363 }
364 InputEvent::Pointer(PointerEvent::Move { point, .. }) => {
365 if let Some(focused_id) = ctx.interaction.focused {
366 if let Some(node) = ctx.ir.nodes.get(&focused_id) {
367 if let Op::Semantics(sem) = &node.op {
368 if sem.role == fission_ir::semantics::Role::TextInput {
369 let active_handle = ctx
370 .text_edit
371 .states
372 .get(&focused_id)
373 .and_then(|state| state.affordances.active_handle);
374 if let Some(active_handle) = active_handle {
375 if let Some((scroll_id, text_op_node_id, scroll_direction)) =
376 Self::find_scroll_container_and_text_op(
377 ctx.ir,
378 focused_id,
379 sem.multiline,
380 )
381 {
382 if let Some(scroll_geom) =
383 ctx.layout.get_node_geometry(scroll_id)
384 {
385 let value = sem.value.as_deref().unwrap_or("");
386 let display_value = Self::display_value_for_metrics(
387 ctx, focused_id, value,
388 );
389 let metric_text = if sem.masked {
390 Self::mask_text_for_metrics(&display_value)
391 } else {
392 display_value.clone()
393 };
394 let new_caret = if let Some(measurer) = ctx.measurer {
395 let local_point =
396 Self::text_local_point_from_screen(
397 ctx,
398 scroll_id,
399 scroll_direction,
400 scroll_geom,
401 *point,
402 );
403 let masked_caret = Self::hit_test_text(
404 measurer,
405 ctx.layout,
406 ctx.ir,
407 focused_id,
408 text_op_node_id,
409 sem.masked,
410 &metric_text,
411 scroll_geom,
412 local_point.x,
413 local_point.y,
414 );
415 if sem.masked {
416 Self::source_byte_offset_from_masked(
417 &display_value,
418 &metric_text,
419 masked_caret,
420 )
421 } else {
422 masked_caret
423 }
424 } else {
425 0
426 };
427 let (caret, anchor) = {
428 let st =
429 ctx.text_edit.get_mut_or_default(focused_id);
430 match active_handle {
431 TextSelectionHandleKind::Caret => {
432 st.caret = new_caret;
433 st.anchor = new_caret;
434 }
435 TextSelectionHandleKind::Start => {
436 if st.caret <= st.anchor {
437 st.caret = new_caret;
438 } else {
439 st.anchor = new_caret;
440 }
441 }
442 TextSelectionHandleKind::End => {
443 if st.caret >= st.anchor {
444 st.caret = new_caret;
445 } else {
446 st.anchor = new_caret;
447 }
448 }
449 }
450 (st.caret, st.anchor)
451 };
452 Self::auto_scroll_textinput(ctx, focused_id);
453 Self::dispatch_cursor_change(
454 ctx, sem, focused_id, caret, anchor,
455 );
456 Self::sync_text_input_affordances(
457 ctx, focused_id, sem, value, false, None,
458 );
459 }
460 }
461 return true;
462 }
463
464 if ctx.interaction.is_pressed(focused_id) {
465 let moved_enough =
466 match Self::drag_start_behavior(ctx, focused_id) {
467 DragStartBehavior::Down => true,
468 DragStartBehavior::Start => {
469 let mut moved_enough = true;
470 if let Some(start) = ctx.interaction.last_down_point
471 {
472 let dx = point.x - start.x;
473 let dy = point.y - start.y;
474 if dx * dx + dy * dy < 4.0 {
475 moved_enough = false;
476 }
477 }
478 moved_enough
479 }
480 };
481 if moved_enough {
482 if let Some((
483 scroll_id,
484 text_op_node_id,
485 scroll_direction,
486 )) = Self::find_scroll_container_and_text_op(
487 ctx.ir,
488 focused_id,
489 sem.multiline,
490 ) {
491 if let Some(scroll_geom) =
492 ctx.layout.get_node_geometry(scroll_id)
493 {
494 let value = sem.value.as_deref().unwrap_or("");
495 let display_value = Self::display_value_for_metrics(
496 ctx, focused_id, value,
497 );
498 let metric_text = if sem.masked {
499 Self::mask_text_for_metrics(&display_value)
500 } else {
501 display_value.clone()
502 };
503 let new_caret = if let Some(measurer) = ctx.measurer
504 {
505 let local_point =
506 Self::text_local_point_from_screen(
507 ctx,
508 scroll_id,
509 scroll_direction,
510 scroll_geom,
511 *point,
512 );
513
514 let masked_caret = Self::hit_test_text(
515 measurer,
516 ctx.layout,
517 ctx.ir,
518 focused_id,
519 text_op_node_id,
520 sem.masked,
521 &metric_text,
522 scroll_geom,
523 local_point.x,
524 local_point.y,
525 );
526 if sem.masked {
527 Self::source_byte_offset_from_masked(
528 &display_value,
529 &metric_text,
530 masked_caret,
531 )
532 } else {
533 masked_caret
534 }
535 } else {
536 let font_size =
537 Self::extract_font_size(ctx.ir, focused_id)
538 .unwrap_or(13.0);
539 Self::caret_from_point_in_text_fallback(
540 &display_value,
541 font_size,
542 scroll_geom.rect.origin.x,
543 scroll_geom.rect.size.width,
544 scroll_geom.content_size.width,
545 ctx.scroll.get_offset(scroll_id),
546 point.x,
547 )
548 };
549 let st =
550 ctx.text_edit.get_mut_or_default(focused_id);
551 st.caret = new_caret;
552 let current_anchor = st.anchor;
553 Self::auto_scroll_textinput(ctx, focused_id);
554 Self::dispatch_cursor_change(
555 ctx,
556 sem,
557 focused_id,
558 new_caret,
559 current_anchor,
560 );
561 Self::sync_text_input_affordances(
562 ctx, focused_id, sem, value, false, None,
563 );
564 }
565 }
566 }
567 }
568 return true;
569 }
570 }
571 }
572 }
573
574 false
575 }
576 InputEvent::Pointer(PointerEvent::Up { point, button, .. }) => {
577 if let Some(focused_id) = ctx.interaction.focused {
578 if let Some(node) = ctx.ir.nodes.get(&focused_id) {
579 if let Op::Semantics(sem) = &node.op {
580 if sem.role == fission_ir::semantics::Role::TextInput {
581 let value = sem.value.as_deref().unwrap_or("").to_string();
582 let toolbar_anchor = Self::input_wrapper_geometry(ctx, focused_id)
583 .map(|geom| {
584 fission_layout::LayoutPoint::new(
585 (point.x - geom.rect.origin.x).max(0.0),
586 (point.y - geom.rect.origin.y).max(0.0),
587 )
588 });
589 let show_toolbar =
590 matches!(button, crate::event::PointerButton::Secondary)
591 || ctx
592 .text_edit
593 .states
594 .get(&focused_id)
595 .map(|state| state.caret != state.anchor)
596 .unwrap_or(false);
597 if let Some(state) = ctx.text_edit.states.get_mut(&focused_id) {
598 state.affordances.active_handle = None;
599 state.affordances.magnifier_visible = false;
600 }
601 Self::sync_text_input_affordances(
602 ctx,
603 focused_id,
604 sem,
605 &value,
606 show_toolbar,
607 if show_toolbar { toolbar_anchor } else { None },
608 );
609 return true;
610 }
611 }
612 }
613 }
614
615 false
616 }
617 _ => false,
618 }
619 }
620}
621
622impl TextInputController {
623 fn handle_text_edit_command(
624 &mut self,
625 ctx: &mut ControllerContext,
626 command: crate::TextEditCommand,
627 ) -> bool {
628 let Some(focused_id) = ctx.interaction.focused else {
629 return false;
630 };
631 self.handle_text_edit_command_for(ctx, focused_id, command)
632 }
633
634 pub(crate) fn handle_text_edit_command_for(
635 &mut self,
636 ctx: &mut ControllerContext,
637 focused_id: WidgetId,
638 command: crate::TextEditCommand,
639 ) -> bool {
640 let Some(semantics) = Self::text_input_semantics(ctx, focused_id) else {
641 return false;
642 };
643 if semantics.disabled || semantics.read_only {
644 return true;
645 }
646 Self::sync_runtime_state(
647 ctx,
648 focused_id,
649 semantics.value.as_deref().unwrap_or_default(),
650 );
651 let runtime = Self::runtime_config(ctx, focused_id);
652 let old_value = ctx.text_edit.get_mut_or_default(focused_id).editing_value();
653 let mut formatters = semantics.input_formatters.clone();
654 if !semantics.multiline && !formatters.contains(&InputFormatter::SingleLine) {
655 formatters.push(InputFormatter::SingleLine);
656 }
657 let pipeline = crate::TextEditPipeline {
658 formatters,
659 custom_formatters: runtime
660 .map(|config| config.custom_input_formatters)
661 .unwrap_or_default(),
662 max_length: semantics.max_length,
663 max_length_enforcement: semantics.max_length_enforcement,
664 };
665 let Ok(result) = pipeline.apply(&old_value, command) else {
666 return true;
667 };
668 if matches!(result.phase, crate::TextEditPhase::Submitted) {
669 Self::dispatch_action_for_trigger(
670 ctx,
671 &semantics,
672 focused_id,
673 fission_ir::semantics::ActionTrigger::Submit,
674 None,
675 );
676 return true;
677 }
678 if matches!(result.phase, crate::TextEditPhase::EditingCompleted) {
679 Self::dispatch_action_for_trigger(
680 ctx,
681 &semantics,
682 focused_id,
683 fission_ir::semantics::ActionTrigger::EditingComplete,
684 None,
685 );
686 return true;
687 }
688 let source = result.source;
689 let mut caret = result.new_value.selection.extent.utf8_offset();
690 let mut anchor = result.new_value.selection.base.utf8_offset();
691 match result.phase {
692 crate::TextEditPhase::Selection => {
693 let state = ctx.text_edit.get_mut_or_default(focused_id);
694 state.caret = caret;
695 state.anchor = anchor;
696 }
697 crate::TextEditPhase::CompositionCancelled => {
698 let state = ctx.text_edit.get_mut_or_default(focused_id);
699 state.clear_preedit();
700 state.pending_model_sync = true;
704 state.last_model_text = semantics.value.clone().unwrap_or_default();
705 caret = state.caret;
706 anchor = state.anchor;
707 }
708 crate::TextEditPhase::CompositionStarted | crate::TextEditPhase::CompositionUpdated => {
709 let state = ctx.text_edit.get_mut_or_default(focused_id);
710 state.sync_composing_value(result.new_value);
711 }
712 _ => {
713 ctx.text_edit
714 .get_mut_or_default(focused_id)
715 .apply_editing_value(result.new_value.clone());
716 self.dispatch_edit_result(ctx, &semantics, focused_id, result);
717 }
718 }
719 Self::dispatch_cursor_change_from(ctx, &semantics, focused_id, caret, anchor, source);
720 Self::auto_scroll_textinput(ctx, focused_id);
721 true
722 }
723
724 fn handle_editing_command(
725 &mut self,
726 ctx: &mut ControllerContext,
727 command: &EditingCommand,
728 ) -> bool {
729 let Some(focused_id) = ctx.interaction.focused else {
730 return false;
731 };
732 let Some(semantics) = Self::text_input_semantics(ctx, focused_id) else {
733 return false;
734 };
735 if semantics.disabled {
736 return false;
737 }
738
739 let (value, caret, anchor) =
740 Self::resolve_editing_value(ctx, focused_id, semantics.value.as_deref().unwrap_or(""));
741 let caret = Self::clamp_caret_to_value(&value, caret);
742 let anchor = Self::clamp_caret_to_value(&value, anchor);
743 let selection = (caret != anchor).then_some((caret.min(anchor), caret.max(anchor)));
744
745 match command {
746 EditingCommand::Copy => {
747 if let (Some((start, end)), Some(clipboard)) = (selection, ctx.clipboard) {
748 clipboard.set_text(&value[start..end]);
749 }
750 }
751 EditingCommand::Cut => {
752 if let Some((start, end)) = selection {
753 if let Some(clipboard) = ctx.clipboard {
754 clipboard.set_text(&value[start..end]);
755 }
756 if !semantics.read_only {
757 if let Some(result) = Self::apply_text_edit_transaction(
758 ctx,
759 &semantics,
760 focused_id,
761 start..end,
762 String::new(),
763 crate::TextEditSource::Clipboard,
764 ) {
765 let caret = result.new_value.selection.extent.utf8_offset();
766 let anchor = result.new_value.selection.base.utf8_offset();
767 self.dispatch_edit_result(ctx, &semantics, focused_id, result);
768 Self::dispatch_cursor_change(
769 ctx, &semantics, focused_id, caret, anchor,
770 );
771 }
772 }
773 }
774 }
775 EditingCommand::Paste(text) => {
776 if !semantics.read_only && !text.is_empty() {
777 let (start, end) = selection.unwrap_or((caret, caret));
778 if let Some(inserted) =
779 Self::prepare_inserted_text(&semantics, &value, start, end, text)
780 {
781 if let Some(result) = Self::apply_text_edit_transaction(
782 ctx,
783 &semantics,
784 focused_id,
785 start..end,
786 inserted,
787 crate::TextEditSource::Clipboard,
788 ) {
789 let caret = result.new_value.selection.extent.utf8_offset();
790 let anchor = result.new_value.selection.base.utf8_offset();
791 self.dispatch_edit_result(ctx, &semantics, focused_id, result);
792 Self::dispatch_cursor_change(
793 ctx, &semantics, focused_id, caret, anchor,
794 );
795 }
796 }
797 }
798 }
799 EditingCommand::SelectAll => {
800 let state = ctx.text_edit.get_mut_or_default(focused_id);
801 state.caret = value.len();
802 state.anchor = 0;
803 state.clear_preedit();
804 Self::dispatch_cursor_change(ctx, &semantics, focused_id, value.len(), 0);
805 }
806 EditingCommand::Undo | EditingCommand::Redo => {
807 let edit = {
808 let state = ctx.text_edit.get_mut_or_default(focused_id);
809 match command {
810 EditingCommand::Undo => state.undo(),
811 EditingCommand::Redo => state.redo(),
812 _ => unreachable!(),
813 }
814 };
815 if let Some((next, next_caret, next_anchor)) = edit {
816 self.dispatch_change(ctx, &semantics, focused_id, next);
817 Self::dispatch_cursor_change(
818 ctx,
819 &semantics,
820 focused_id,
821 next_caret,
822 next_anchor,
823 );
824 }
825 }
826 }
827
828 let displayed_value = ctx
829 .text_edit
830 .get(focused_id)
831 .map(|state| state.committed_text().to_owned())
832 .unwrap_or(value);
833 Self::sync_text_input_affordances(
834 ctx,
835 focused_id,
836 &semantics,
837 displayed_value.as_str(),
838 false,
839 None,
840 );
841 true
842 }
843
844 fn text_input_semantics(ctx: &ControllerContext, focused_id: WidgetId) -> Option<Semantics> {
845 let mut current_id = Some(focused_id);
846 while let Some(node_id) = current_id {
847 let node = ctx.ir.nodes.get(&node_id)?;
848 if let Op::Semantics(semantics) = &node.op {
849 if semantics.role == fission_ir::semantics::Role::TextInput {
850 return Some(semantics.clone());
851 }
852 }
853 current_id = node.parent;
854 }
855 None
856 }
857
858 fn handle_key_with_produced_text(
859 &mut self,
860 ctx: &mut ControllerContext,
861 key_code: KeyCode,
862 modifiers: u8,
863 text: &str,
864 ) -> bool {
865 let shortcut = ctx.editing_convention.has_primary_shortcut(modifiers)
866 && !ctx.editing_convention.is_alt_gr(modifiers);
867 let text_key = matches!(key_code, KeyCode::Char(_) | KeyCode::Space);
868 if shortcut || !text_key || text.is_empty() {
869 return self.handle_key(ctx, key_code, modifiers);
870 }
871 let Some(focused_id) = ctx.interaction.focused else {
872 return false;
873 };
874 let Some(semantics) = Self::text_input_semantics(ctx, focused_id) else {
875 return false;
876 };
877 if semantics.disabled || semantics.read_only {
878 return true;
879 }
880 Self::sync_runtime_state(
881 ctx,
882 focused_id,
883 semantics.value.as_deref().unwrap_or_default(),
884 );
885 let value = ctx.text_edit.get_mut_or_default(focused_id).editing_value();
886 self.handle_text_edit_command(
887 ctx,
888 crate::TextEditCommand::Replace {
889 range: value.selection_range(),
890 text: text.to_string(),
891 source: crate::TextEditSource::Keyboard,
892 },
893 )
894 }
895
896 fn handle_key(
897 &mut self,
898 ctx: &mut ControllerContext,
899 key_code: KeyCode,
900 modifiers: u8,
901 ) -> bool {
902 let focused_id = if let Some(id) = ctx.interaction.focused {
903 id
904 } else {
905 return false;
906 };
907
908 let mut semantics_node = None;
909 let mut current_id = Some(focused_id);
910 while let Some(node_id) = current_id {
911 if let Some(node) = ctx.ir.nodes.get(&node_id) {
912 if let Op::Semantics(s) = &node.op {
913 if s.role == fission_ir::semantics::Role::TextInput {
914 semantics_node = Some(s);
915 break;
916 }
917 }
918 current_id = node.parent;
919 } else {
920 break;
921 }
922 }
923
924 let semantics = if let Some(s) = semantics_node {
925 s
926 } else {
927 return false;
928 };
929
930 let (value, mut caret, mut anchor) =
931 Self::resolve_editing_value(ctx, focused_id, semantics.value.as_deref().unwrap_or(""));
932 if let Some(st) = ctx.text_edit.states.get_mut(&focused_id) {
933 st.clear_preedit();
934 }
935
936 caret = Self::clamp_caret_to_value(&value, caret);
937 anchor = Self::clamp_caret_to_value(&value, anchor);
938
939 let sel = if caret != anchor {
940 Some((caret.min(anchor), caret.max(anchor)))
941 } else {
942 None
943 };
944
945 let mut next_caret = caret;
947 let mut next_anchor = anchor;
948 let mut next_edit: Option<(std::ops::Range<usize>, String)> = None;
949 let mut handled = false;
950
951 let read_only = semantics.read_only;
952 let disabled = semantics.disabled;
953 let convention = ctx.editing_convention;
954 let is_apple = convention.is_apple();
955 let shift = Self::has_shift(modifiers);
956 let primary_shortcut =
957 convention.has_primary_shortcut(modifiers) && !convention.is_alt_gr(modifiers);
958 let word_modifier = convention.has_word_modifier(modifiers);
959
960 if disabled {
961 return false;
962 }
963
964 match key_code {
965 KeyCode::Space => {
966 if read_only {
967 handled = true;
968 } else {
969 let (s, e) = sel.unwrap_or((caret, caret));
970 if let Some(inserted) =
971 Self::prepare_inserted_text(semantics, &value, s, e, " ")
972 {
973 next_caret = s + inserted.len();
974 next_anchor = next_caret;
975 next_edit = Some((s..e, inserted));
976 }
977 handled = true;
978 }
979 }
980 KeyCode::Char(ch) => {
981 let lower = ch.to_ascii_lowercase();
982 if primary_shortcut {
983 let command = match lower {
984 'a' => Some(EditingCommand::SelectAll),
985 'c' => Some(EditingCommand::Copy),
986 'x' => Some(EditingCommand::Cut),
987 'v' => Some(EditingCommand::Paste(
988 ctx.clipboard
989 .and_then(|clipboard| clipboard.get_text())
990 .unwrap_or_default(),
991 )),
992 'z' if shift => Some(EditingCommand::Redo),
993 'z' => Some(EditingCommand::Undo),
994 'y' if !is_apple => Some(EditingCommand::Redo),
995 _ => None,
996 };
997 return command
998 .map_or(true, |command| self.handle_editing_command(ctx, &command));
999 }
1000
1001 if !handled
1002 && is_apple
1003 && Self::has_ctrl(modifiers)
1004 && !Self::has_alt(modifiers)
1005 && !Self::has_super(modifiers)
1006 {
1007 match lower {
1008 'a' => {
1009 let (line_start, _) = Self::current_line_bounds(
1010 ctx, focused_id, semantics, &value, caret,
1011 );
1012 next_caret = line_start;
1013 next_anchor = if shift { anchor } else { line_start };
1014 handled = true;
1015 }
1016 'e' => {
1017 let (_, line_end) = Self::current_line_bounds(
1018 ctx, focused_id, semantics, &value, caret,
1019 );
1020 next_caret = line_end;
1021 next_anchor = if shift { anchor } else { line_end };
1022 handled = true;
1023 }
1024 'f' => {
1025 let next = Self::next_grapheme_boundary(&value, caret);
1026 next_caret = next;
1027 next_anchor = if shift { anchor } else { next };
1028 handled = true;
1029 }
1030 'b' => {
1031 let prev = Self::prev_grapheme_boundary(&value, caret);
1032 next_caret = prev;
1033 next_anchor = if shift { anchor } else { prev };
1034 handled = true;
1035 }
1036 'n' if semantics.multiline => {
1037 self.handle_vertical_navigation(
1038 ctx, focused_id, semantics, &value, caret, modifiers, false,
1039 );
1040 return true;
1041 }
1042 'p' if semantics.multiline => {
1043 self.handle_vertical_navigation(
1044 ctx, focused_id, semantics, &value, caret, modifiers, true,
1045 );
1046 return true;
1047 }
1048 'h' => {
1049 handled = true;
1050 if !read_only {
1051 let (s, e) = sel.unwrap_or_else(|| {
1052 if caret == 0 {
1053 (0, 0)
1054 } else {
1055 (Self::prev_grapheme_boundary(&value, caret), caret)
1056 }
1057 });
1058 next_edit = Some((s..e, String::new()));
1059 next_caret = s;
1060 next_anchor = s;
1061 }
1062 }
1063 'd' => {
1064 handled = true;
1065 if !read_only {
1066 let (s, e) = sel.unwrap_or_else(|| {
1067 let next = Self::next_grapheme_boundary(&value, caret);
1068 (caret, next)
1069 });
1070 next_edit = Some((s..e, String::new()));
1071 next_caret = s;
1072 next_anchor = s;
1073 }
1074 }
1075 _ => {}
1076 }
1077 }
1078
1079 if !handled {
1080 if read_only {
1081 handled = true;
1082 } else {
1083 let (s, e) = sel.unwrap_or((caret, caret));
1084 if let Some(inserted) =
1085 Self::prepare_inserted_text(semantics, &value, s, e, &ch.to_string())
1086 {
1087 next_caret = s + inserted.len();
1088 next_anchor = next_caret;
1089 next_edit = Some((s..e, inserted));
1090 }
1091 handled = true;
1092 }
1093 }
1094 }
1095 KeyCode::Backspace => {
1096 handled = true;
1097 if !read_only {
1098 let (s, e) = if let Some((s, e)) = sel {
1099 (s, e)
1100 } else if is_apple && Self::has_super(modifiers) {
1101 let (line_start, _) =
1102 Self::current_line_bounds(ctx, focused_id, semantics, &value, caret);
1103 (line_start, caret)
1104 } else if word_modifier {
1105 (Self::prev_word_boundary(&value, caret), caret)
1106 } else if caret == 0 {
1107 (0, 0)
1108 } else {
1109 (Self::prev_grapheme_boundary(&value, caret), caret)
1110 };
1111 next_edit = Some((s..e, String::new()));
1112 next_caret = s;
1113 next_anchor = s;
1114 }
1115 }
1116 KeyCode::Delete => {
1117 handled = true;
1118 if !read_only {
1119 let (s, e) = if let Some((s, e)) = sel {
1120 (s, e)
1121 } else if is_apple && Self::has_super(modifiers) {
1122 let (_, line_end) =
1123 Self::current_line_bounds(ctx, focused_id, semantics, &value, caret);
1124 (caret, line_end)
1125 } else if word_modifier {
1126 (caret, Self::next_word_boundary(&value, caret))
1127 } else {
1128 let next = Self::next_grapheme_boundary(&value, caret);
1129 (caret, next)
1130 };
1131 next_edit = Some((s..e, String::new()));
1132 next_caret = s;
1133 next_anchor = s;
1134 }
1135 }
1136 KeyCode::Left => {
1137 let prev = if let Some((s, _)) = sel {
1138 if !shift && !word_modifier && !(is_apple && Self::has_super(modifiers)) {
1139 s
1140 } else if is_apple && Self::has_super(modifiers) {
1141 Self::current_line_bounds(ctx, focused_id, semantics, &value, caret).0
1142 } else if word_modifier {
1143 Self::prev_word_boundary(&value, caret)
1144 } else {
1145 Self::prev_grapheme_boundary(&value, caret)
1146 }
1147 } else if is_apple && Self::has_super(modifiers) {
1148 Self::current_line_bounds(ctx, focused_id, semantics, &value, caret).0
1149 } else if word_modifier {
1150 Self::prev_word_boundary(&value, caret)
1151 } else {
1152 Self::prev_grapheme_boundary(&value, caret)
1153 };
1154 next_caret = prev;
1155 next_anchor = if shift { anchor } else { prev };
1156 handled = true;
1157 }
1158 KeyCode::Right => {
1159 let next = if let Some((_, e)) = sel {
1160 if !shift && !word_modifier && !(is_apple && Self::has_super(modifiers)) {
1161 e
1162 } else if is_apple && Self::has_super(modifiers) {
1163 Self::current_line_bounds(ctx, focused_id, semantics, &value, caret).1
1164 } else if word_modifier {
1165 Self::next_word_boundary(&value, caret)
1166 } else {
1167 Self::next_grapheme_boundary(&value, caret)
1168 }
1169 } else if is_apple && Self::has_super(modifiers) {
1170 Self::current_line_bounds(ctx, focused_id, semantics, &value, caret).1
1171 } else if word_modifier {
1172 Self::next_word_boundary(&value, caret)
1173 } else {
1174 Self::next_grapheme_boundary(&value, caret)
1175 };
1176 next_caret = next;
1177 next_anchor = if shift { anchor } else { next };
1178 handled = true;
1179 }
1180 KeyCode::Home => {
1181 next_caret = if semantics.multiline && !(Self::has_ctrl(modifiers) && !is_apple) {
1182 Self::current_line_bounds(ctx, focused_id, semantics, &value, caret).0
1183 } else {
1184 0
1185 };
1186 next_anchor = if shift { anchor } else { next_caret };
1187 handled = true;
1188 }
1189 KeyCode::End => {
1190 next_caret = if semantics.multiline && !(Self::has_ctrl(modifiers) && !is_apple) {
1191 Self::current_line_bounds(ctx, focused_id, semantics, &value, caret).1
1192 } else {
1193 value.len()
1194 };
1195 next_anchor = if shift { anchor } else { next_caret };
1196 handled = true;
1197 }
1198 KeyCode::Enter => {
1199 if semantics.multiline {
1200 handled = true;
1201 if !read_only {
1202 let insert_str = if semantics.auto_indent {
1203 let line_start = value[..caret].rfind('\n').map(|p| p + 1).unwrap_or(0);
1204 let leading: String = value[line_start..]
1205 .chars()
1206 .take_while(|c| *c == ' ' || *c == '\t')
1207 .collect();
1208 format!("\n{}", leading)
1209 } else {
1210 "\n".to_string()
1211 };
1212 let (s, e) = sel.unwrap_or((caret, caret));
1213 if let Some(inserted) =
1214 Self::prepare_inserted_text(semantics, &value, s, e, &insert_str)
1215 {
1216 next_caret = s + inserted.len();
1217 next_anchor = next_caret;
1218 next_edit = Some((s..e, inserted));
1219 }
1220 }
1221 } else if Self::dispatch_submit(ctx, semantics, focused_id, &value) {
1222 return true;
1223 }
1224 }
1225 KeyCode::Up => {
1226 if is_apple && Self::has_super(modifiers) {
1227 next_caret = 0;
1228 next_anchor = if shift { anchor } else { 0 };
1229 handled = true;
1230 } else if semantics.multiline {
1231 self.handle_vertical_navigation(
1232 ctx, focused_id, semantics, &value, caret, modifiers, true,
1233 );
1234 return true;
1235 }
1236 }
1237 KeyCode::Down => {
1238 if is_apple && Self::has_super(modifiers) {
1239 next_caret = value.len();
1240 next_anchor = if shift { anchor } else { value.len() };
1241 handled = true;
1242 } else if semantics.multiline {
1243 self.handle_vertical_navigation(
1244 ctx, focused_id, semantics, &value, caret, modifiers, false,
1245 );
1246 return true;
1247 }
1248 }
1249 KeyCode::PageUp => {
1250 if semantics.multiline {
1251 self.handle_page_navigation(
1252 ctx, focused_id, semantics, &value, caret, modifiers, true,
1253 );
1254 return true;
1255 }
1256 }
1257 KeyCode::PageDown => {
1258 if semantics.multiline {
1259 self.handle_page_navigation(
1260 ctx, focused_id, semantics, &value, caret, modifiers, false,
1261 );
1262 return true;
1263 }
1264 }
1265 KeyCode::Tab => {
1266 if semantics.capture_tab {
1267 handled = true;
1268 if !read_only {
1269 let tab_str = " ";
1270 let (s, e) = sel.unwrap_or((caret, caret));
1271 if let Some(inserted) =
1272 Self::prepare_inserted_text(semantics, &value, s, e, tab_str)
1273 {
1274 next_caret = s + inserted.len();
1275 next_anchor = next_caret;
1276 next_edit = Some((s..e, inserted));
1277 }
1278 }
1279 }
1280 }
1281 _ => {}
1282 }
1283
1284 if let Some((range, replacement)) = next_edit {
1285 if let Some(result) = Self::apply_text_edit_transaction(
1286 ctx,
1287 semantics,
1288 focused_id,
1289 range,
1290 replacement,
1291 crate::TextEditSource::Keyboard,
1292 ) {
1293 next_caret = result.new_value.selection.extent.utf8_offset();
1294 next_anchor = result.new_value.selection.base.utf8_offset();
1295 self.dispatch_edit_result(ctx, semantics, focused_id, result);
1296 Self::dispatch_cursor_change(ctx, semantics, focused_id, next_caret, next_anchor);
1297 }
1298 Self::sync_text_input_affordances(
1299 ctx,
1300 focused_id,
1301 semantics,
1302 value.as_str(),
1303 false,
1304 None,
1305 );
1306 } else if handled {
1307 let st = ctx.text_edit.get_mut_or_default(focused_id);
1309 st.caret = next_caret;
1310 st.anchor = next_anchor;
1311 st.clear_preedit();
1312 Self::auto_scroll_textinput(ctx, focused_id);
1313 Self::dispatch_cursor_change(ctx, semantics, focused_id, next_caret, next_anchor);
1314 Self::sync_text_input_affordances(
1315 ctx,
1316 focused_id,
1317 semantics,
1318 value.as_str(),
1319 false,
1320 None,
1321 );
1322 }
1323
1324 handled
1325 }
1326
1327 fn runtime_config(
1328 ctx: &ControllerContext,
1329 focused_id: WidgetId,
1330 ) -> Option<crate::ui::widgets::text_input::TextInputRuntimeConfig> {
1331 ctx.ir
1332 .custom_render_objects
1333 .get(&focused_id)
1334 .and_then(downcast_text_input_runtime_config)
1335 .cloned()
1336 }
1337
1338 fn drag_start_behavior(ctx: &ControllerContext, focused_id: WidgetId) -> DragStartBehavior {
1339 Self::runtime_config(ctx, focused_id)
1340 .map(|cfg| cfg.drag_start_behavior)
1341 .unwrap_or_default()
1342 }
1343
1344 fn sync_runtime_state(ctx: &mut ControllerContext, focused_id: WidgetId, semantic_value: &str) {
1345 let runtime = Self::runtime_config(ctx, focused_id);
1346 let masked =
1347 Self::text_input_semantics(ctx, focused_id).is_some_and(|semantics| semantics.masked);
1348 ctx.text_edit.sync_from_runtime(
1349 focused_id,
1350 semantic_value,
1351 runtime
1352 .as_ref()
1353 .and_then(|cfg| cfg.restoration_id.as_deref()),
1354 runtime
1355 .as_ref()
1356 .and_then(|cfg| cfg.undo_controller.as_ref().map(|undo| undo.capacity)),
1357 masked,
1358 );
1359 }
1360
1361 fn persist_runtime_state(ctx: &mut ControllerContext, focused_id: WidgetId) {
1362 let runtime = Self::runtime_config(ctx, focused_id);
1363 let masked =
1364 Self::text_input_semantics(ctx, focused_id).is_some_and(|semantics| semantics.masked);
1365 ctx.text_edit.persist_restoration(
1366 focused_id,
1367 runtime
1368 .as_ref()
1369 .and_then(|cfg| cfg.restoration_id.as_deref()),
1370 masked,
1371 );
1372 }
1373
1374 fn has_shift(modifiers: u8) -> bool {
1375 (modifiers & MOD_SHIFT) != 0
1376 }
1377
1378 fn has_alt(modifiers: u8) -> bool {
1379 (modifiers & MOD_ALT) != 0
1380 }
1381
1382 fn has_ctrl(modifiers: u8) -> bool {
1383 (modifiers & MOD_CTRL) != 0
1384 }
1385
1386 fn has_super(modifiers: u8) -> bool {
1387 (modifiers & MOD_SUPER) != 0
1388 }
1389
1390 fn node_or_ancestor_matches(
1391 ir: &fission_ir::CoreIR,
1392 node_id: WidgetId,
1393 expected: WidgetId,
1394 ) -> bool {
1395 let mut current = Some(node_id);
1396 while let Some(id) = current {
1397 if id == expected {
1398 return true;
1399 }
1400 current = ir.nodes.get(&id).and_then(|node| node.parent);
1401 }
1402 false
1403 }
1404
1405 fn toolbar_action_hit(
1406 ir: &fission_ir::CoreIR,
1407 focused_id: WidgetId,
1408 hit_node_id: WidgetId,
1409 ) -> Option<TextContextMenuAction> {
1410 for action in [
1411 TextContextMenuAction::Copy,
1412 TextContextMenuAction::Cut,
1413 TextContextMenuAction::Paste,
1414 TextContextMenuAction::SelectAll,
1415 ] {
1416 if Self::node_or_ancestor_matches(
1417 ir,
1418 hit_node_id,
1419 text_input_toolbar_button_id(focused_id, action),
1420 ) {
1421 return Some(action);
1422 }
1423 }
1424 None
1425 }
1426
1427 fn selection_handle_hit(
1428 ir: &fission_ir::CoreIR,
1429 focused_id: WidgetId,
1430 hit_node_id: WidgetId,
1431 ) -> Option<TextSelectionHandleKind> {
1432 for kind in [
1433 TextSelectionHandleKind::Caret,
1434 TextSelectionHandleKind::Start,
1435 TextSelectionHandleKind::End,
1436 ] {
1437 if Self::node_or_ancestor_matches(
1438 ir,
1439 hit_node_id,
1440 text_input_selection_handle_id(focused_id, kind),
1441 ) {
1442 return Some(kind);
1443 }
1444 }
1445 None
1446 }
1447
1448 fn execute_toolbar_action(
1449 &mut self,
1450 ctx: &mut ControllerContext,
1451 action: TextContextMenuAction,
1452 ) -> bool {
1453 let command = match action {
1454 TextContextMenuAction::Copy => EditingCommand::Copy,
1455 TextContextMenuAction::Cut => EditingCommand::Cut,
1456 TextContextMenuAction::Paste => EditingCommand::Paste(
1457 ctx.clipboard
1458 .and_then(|clipboard| clipboard.get_text())
1459 .unwrap_or_default(),
1460 ),
1461 TextContextMenuAction::SelectAll => EditingCommand::SelectAll,
1462 };
1463 self.handle_editing_command(ctx, &command)
1464 }
1465
1466 fn prepare_inserted_text(
1467 semantics: &Semantics,
1468 _current_value: &str,
1469 _replace_start: usize,
1470 _replace_end: usize,
1471 raw_text: &str,
1472 ) -> Option<String> {
1473 let mut inserted = raw_text.to_string();
1476 if !semantics.multiline {
1477 inserted = inserted.replace(['\r', '\n'], "");
1478 }
1479
1480 if let Some(mask) = &semantics.input_mask {
1481 inserted = inserted
1482 .chars()
1483 .filter(|ch| mask.is_valid_char(*ch))
1484 .collect();
1485 }
1486
1487 if inserted.is_empty() {
1488 None
1489 } else {
1490 Some(inserted)
1491 }
1492 }
1493
1494 fn apply_text_edit_transaction(
1495 ctx: &mut ControllerContext,
1496 semantics: &Semantics,
1497 node_id: WidgetId,
1498 range: std::ops::Range<usize>,
1499 replacement: String,
1500 source: crate::TextEditSource,
1501 ) -> Option<crate::TextEditResult> {
1502 let runtime = Self::runtime_config(ctx, node_id);
1503 let old_value = ctx.text_edit.get_mut_or_default(node_id).editing_value();
1504 let range = crate::TextRange::new(&old_value.text, range.start, range.end).ok()?;
1505 let mut formatters = semantics.input_formatters.clone();
1506 if !semantics.multiline && !formatters.contains(&InputFormatter::SingleLine) {
1507 formatters.push(InputFormatter::SingleLine);
1508 }
1509 let pipeline = crate::TextEditPipeline {
1510 formatters,
1511 custom_formatters: runtime
1512 .map(|config| config.custom_input_formatters)
1513 .unwrap_or_default(),
1514 max_length: semantics.max_length,
1515 max_length_enforcement: semantics.max_length_enforcement,
1516 };
1517 let mut result = pipeline
1518 .apply(
1519 &old_value,
1520 crate::TextEditCommand::Replace {
1521 range,
1522 text: replacement,
1523 source,
1524 },
1525 )
1526 .ok()?;
1527 if source == crate::TextEditSource::Ime {
1528 result.phase = crate::TextEditPhase::CompositionCommitted;
1529 }
1530 if result.new_value == result.old_value {
1531 return None;
1532 }
1533 ctx.text_edit
1534 .get_mut_or_default(node_id)
1535 .apply_editing_value(result.new_value.clone());
1536 Some(result)
1537 }
1538
1539 fn dispatch_edit_result(
1540 &self,
1541 ctx: &mut ControllerContext,
1542 semantics: &Semantics,
1543 node_id: WidgetId,
1544 result: crate::TextEditResult,
1545 ) {
1546 Self::persist_runtime_state(ctx, node_id);
1547 if let Some((envelope, input)) =
1548 crate::input::prepare_scoped_text_input_edit(ctx.ir, semantics, node_id, result)
1549 {
1550 ctx.dispatched_actions.push((node_id, envelope, input));
1551 Self::auto_scroll_textinput(ctx, node_id);
1552 }
1553 }
1554
1555 fn handle_ime(&mut self, ctx: &mut ControllerContext, ime: &crate::event::ImeEvent) -> bool {
1556 match ime {
1557 crate::event::ImeEvent::Commit { text } => {
1558 if let Some(focused_id) = ctx.interaction.focused {
1559 if let Some(node) = ctx.ir.nodes.get(&focused_id) {
1560 if let Op::Semantics(semantics) = &node.op {
1561 if semantics.role == fission_ir::semantics::Role::TextInput {
1562 if semantics.disabled || semantics.read_only {
1563 return true;
1564 }
1565 Self::sync_runtime_state(
1566 ctx,
1567 focused_id,
1568 semantics.value.as_deref().unwrap_or(""),
1569 );
1570 let (value, start, end) = {
1571 let st = ctx.text_edit.get_mut_or_default(focused_id);
1572 if let Some(preedit) = &st.preedit {
1573 let (start, end) = preedit.range;
1574 let committed = st.committed_text();
1575 st.clear_preedit();
1582 (committed, start, end)
1583 } else {
1584 let value = st.editing_value();
1585 let range = value.selection_range();
1586 (
1587 value.text,
1588 range.start.utf8_offset(),
1589 range.end.utf8_offset(),
1590 )
1591 }
1592 };
1593
1594 if let Some(filtered_text) =
1595 Self::prepare_inserted_text(semantics, &value, start, end, text)
1596 {
1597 if let Some(result) = Self::apply_text_edit_transaction(
1598 ctx,
1599 semantics,
1600 focused_id,
1601 start..end,
1602 filtered_text,
1603 crate::TextEditSource::Ime,
1604 ) {
1605 let caret = result.new_value.selection.extent.utf8_offset();
1606 let anchor = result.new_value.selection.base.utf8_offset();
1607 self.dispatch_edit_result(
1608 ctx, semantics, focused_id, result,
1609 );
1610 Self::dispatch_cursor_change(
1611 ctx, semantics, focused_id, caret, anchor,
1612 );
1613 }
1614 } else {
1615 ctx.text_edit.get_mut_or_default(focused_id).clear_preedit();
1616 }
1617
1618 return true;
1619 }
1620 }
1621 }
1622 }
1623 }
1624 crate::event::ImeEvent::Preedit { text, cursor } => {
1625 if let Some(focused_id) = ctx.interaction.focused {
1626 if let Some(node) = ctx.ir.nodes.get(&focused_id) {
1627 if let Op::Semantics(semantics) = &node.op {
1628 if semantics.disabled || semantics.read_only {
1629 return true;
1630 }
1631 Self::sync_runtime_state(
1632 ctx,
1633 focused_id,
1634 semantics.value.as_deref().unwrap_or(""),
1635 );
1636 }
1637 }
1638 let st = ctx.text_edit.get_mut_or_default(focused_id);
1639 st.set_preedit(text.clone(), *cursor);
1640 Self::auto_scroll_textinput(ctx, focused_id);
1641 return true;
1642 }
1643 }
1644 crate::event::ImeEvent::Cancel => {
1645 if let Some(focused_id) = ctx.interaction.focused {
1646 if let Some(node) = ctx.ir.nodes.get(&focused_id) {
1647 if let Op::Semantics(semantics) = &node.op {
1648 if semantics.disabled || semantics.read_only {
1649 return true;
1650 }
1651 Self::sync_runtime_state(
1652 ctx,
1653 focused_id,
1654 semantics.value.as_deref().unwrap_or(""),
1655 );
1656 }
1657 }
1658 let st = ctx.text_edit.get_mut_or_default(focused_id);
1659 st.clear_preedit();
1660 Self::auto_scroll_textinput(ctx, focused_id);
1661 return true;
1662 }
1663 }
1664 }
1665 false
1666 }
1667
1668 fn dispatch_change(
1669 &self,
1670 ctx: &mut ControllerContext,
1671 semantics: &fission_ir::Semantics,
1672 node_id: WidgetId,
1673 new_text: String,
1674 ) {
1675 Self::persist_runtime_state(ctx, node_id);
1676 let (new_caret, new_anchor) = ctx
1677 .text_edit
1678 .get(node_id)
1679 .map(|state| (state.caret, state.anchor))
1680 .unwrap_or((new_text.len(), new_text.len()));
1681 if let Some((envelope, input)) = crate::input::prepare_scoped_text_input_change(
1682 ctx.ir, semantics, node_id, new_text, new_caret, new_anchor,
1683 ) {
1684 ctx.dispatched_actions.push((node_id, envelope, input));
1685
1686 Self::auto_scroll_textinput(ctx, node_id);
1689 }
1690 }
1691
1692 fn dispatch_cursor_change(
1693 ctx: &mut ControllerContext,
1694 semantics: &fission_ir::Semantics,
1695 node_id: WidgetId,
1696 new_caret: usize,
1697 new_anchor: usize,
1698 ) {
1699 Self::dispatch_cursor_change_from(
1700 ctx,
1701 semantics,
1702 node_id,
1703 new_caret,
1704 new_anchor,
1705 crate::TextEditSource::Programmatic,
1706 );
1707 }
1708
1709 fn dispatch_cursor_change_from(
1710 ctx: &mut ControllerContext,
1711 semantics: &fission_ir::Semantics,
1712 node_id: WidgetId,
1713 new_caret: usize,
1714 new_anchor: usize,
1715 source: crate::TextEditSource,
1716 ) {
1717 if let Some(st) = ctx.text_edit.states.get(&node_id) {
1721 if st.last_dispatched_cursor == Some((new_caret, new_anchor)) {
1722 return;
1723 }
1724 }
1725
1726 Self::persist_runtime_state(ctx, node_id);
1727
1728 if let Some(action_entry) = semantics
1729 .actions
1730 .entries
1731 .iter()
1732 .find(|e| e.trigger == fission_ir::semantics::ActionTrigger::CursorChange)
1733 {
1734 if let Some(st) = ctx.text_edit.states.get_mut(&node_id) {
1736 st.last_dispatched_cursor = Some((new_caret, new_anchor));
1737 }
1738
1739 let payload = action_entry.payload_data.clone().unwrap_or_default();
1740 let envelope = ActionEnvelope {
1741 id: ActionId::from_u128(action_entry.action_id),
1742 payload,
1743 };
1744 let mut value = ctx.text_edit.get_mut_or_default(node_id).editing_value();
1745 if let Ok(selection) = crate::TextSelection::new(
1746 &value.text,
1747 new_anchor,
1748 new_caret,
1749 crate::TextAffinity::Downstream,
1750 ) {
1751 value.selection = selection;
1752 }
1753 let input = crate::input::scoped_action_input(
1754 ctx.ir,
1755 node_id,
1756 crate::ActionInput::TextSelectionChanged(crate::action::UpdateTextSelection {
1757 node_id,
1758 value,
1759 source,
1760 }),
1761 );
1762 ctx.dispatched_actions.push((node_id, envelope, input));
1763 }
1764 }
1765
1766 fn dispatch_submit(
1767 ctx: &mut ControllerContext,
1768 semantics: &fission_ir::Semantics,
1769 node_id: WidgetId,
1770 current_value: &str,
1771 ) -> bool {
1772 let mut dispatched = false;
1773 for trigger in [
1774 fission_ir::semantics::ActionTrigger::Validation,
1775 fission_ir::semantics::ActionTrigger::EditingComplete,
1776 fission_ir::semantics::ActionTrigger::Submit,
1777 ] {
1778 dispatched |= Self::dispatch_action_for_trigger(
1779 ctx,
1780 semantics,
1781 node_id,
1782 trigger,
1783 Some(serde_json::to_vec(¤t_value.to_string()).unwrap()),
1784 );
1785 }
1786 dispatched
1787 }
1788
1789 fn dispatch_action_for_trigger(
1790 ctx: &mut ControllerContext,
1791 semantics: &fission_ir::Semantics,
1792 node_id: WidgetId,
1793 trigger: fission_ir::semantics::ActionTrigger,
1794 fallback_payload: Option<Vec<u8>>,
1795 ) -> bool {
1796 let Some(action_entry) = semantics
1797 .actions
1798 .entries
1799 .iter()
1800 .find(|e| e.trigger == trigger)
1801 else {
1802 return false;
1803 };
1804 let payload = action_entry
1805 .payload_data
1806 .clone()
1807 .or(fallback_payload)
1808 .unwrap_or_else(|| serde_json::to_vec(&()).unwrap());
1809 let envelope = ActionEnvelope {
1810 id: ActionId::from_u128(action_entry.action_id),
1811 payload,
1812 };
1813 let dynamic_input = match trigger {
1814 fission_ir::semantics::ActionTrigger::Submit
1815 | fission_ir::semantics::ActionTrigger::EditingComplete
1816 | fission_ir::semantics::ActionTrigger::Validation
1817 | fission_ir::semantics::ActionTrigger::TapOutside
1818 | fission_ir::semantics::ActionTrigger::Focus
1819 | fission_ir::semantics::ActionTrigger::Blur => {
1820 let value = ctx.text_edit.get_mut_or_default(node_id).editing_value();
1821 let (source, phase) = match trigger {
1822 fission_ir::semantics::ActionTrigger::Submit => (
1823 crate::TextEditSource::Keyboard,
1824 crate::TextEditPhase::Submitted,
1825 ),
1826 fission_ir::semantics::ActionTrigger::EditingComplete => (
1827 crate::TextEditSource::Keyboard,
1828 crate::TextEditPhase::EditingCompleted,
1829 ),
1830 fission_ir::semantics::ActionTrigger::Validation => (
1831 crate::TextEditSource::Programmatic,
1832 crate::TextEditPhase::Validated,
1833 ),
1834 fission_ir::semantics::ActionTrigger::TapOutside => (
1835 crate::TextEditSource::Pointer,
1836 crate::TextEditPhase::TapOutside,
1837 ),
1838 fission_ir::semantics::ActionTrigger::Focus => (
1839 crate::TextEditSource::Pointer,
1840 crate::TextEditPhase::Focused,
1841 ),
1842 fission_ir::semantics::ActionTrigger::Blur => (
1843 crate::TextEditSource::Pointer,
1844 crate::TextEditPhase::Blurred,
1845 ),
1846 _ => unreachable!(),
1847 };
1848 let mut input = crate::UpdateTextInput::from_values(
1849 node_id,
1850 value.clone(),
1851 value,
1852 source,
1853 phase,
1854 );
1855 if matches!(
1856 trigger,
1857 fission_ir::semantics::ActionTrigger::Submit
1858 | fission_ir::semantics::ActionTrigger::EditingComplete
1859 ) {
1860 input.editing_action = Some(semantics.text_input_action);
1861 }
1862 if trigger == fission_ir::semantics::ActionTrigger::Validation {
1863 input.validation_state = Some(semantics.validation_state);
1864 input.validation_message = semantics.validation_message.clone();
1865 }
1866 crate::ActionInput::TextChanged(input)
1867 }
1868 _ => crate::ActionInput::None,
1869 };
1870 let input = crate::input::scoped_action_input(ctx.ir, node_id, dynamic_input);
1871 ctx.dispatched_actions.push((node_id, envelope, input));
1872 true
1873 }
1874}
1875
1876mod geometry;
1877pub use geometry::caret_from_point_in_text;