1use std::{
7 cell::{Cell, RefCell},
8 rc::{Rc, Weak},
9};
10
11use cranpose_core::{MutableState, NodeId, SideEffect, mutableStateOf, remember};
12use cranpose_foundation::{
13 modifier_element,
14 text::{
15 InputTransformation, OutputTransformation, TextFieldLineLimits, TextFieldState, TextRange,
16 },
17};
18use cranpose_ui_graphics::{Color, Point, Rect};
19
20use crate::{
21 bring_into_view::local_bring_into_view_responder,
22 clipboard_session::{clipboard_can_paste, clipboard_paste_into_focus, clipboard_write_text},
23 composable,
24 keyboard_options::{KeyboardActionHandler, KeyboardOptions},
25 layout::{
26 core::Alignment,
27 policies::{BoxMeasurePolicy, EmptyMeasurePolicy},
28 },
29 modifier::Modifier,
30 safe_area::local_ime_insets,
31 text::{AnnotatedString, TextStyle, measure_text},
32 text_field_decorator_node::TextFieldDecoratorElement,
33 text_field_focus::{dispatch_copy, dispatch_cut, dispatch_select_all},
34 text_field_modifier_node::{
35 TextFieldElement, TextFieldHandleController, TextFieldHandleMetrics, TextFieldRefs,
36 },
37 text_selection::{
38 HANDLE_RADIUS, HandleGrabOffset, HandleKind, LineAffinity, selection_after_handle_drag,
39 },
40 widgets::{
41 CaretActionMenu, Layout, MenuAnchor, SelectionHandle, SelectionLoupe, TextSelectionMenu,
42 loupe_target_for_drag,
43 },
44};
45
46pub const SELECTION_HIGHLIGHT_ALPHA: f32 = 0.32;
51
52const TEXT_LONG_PRESS_MS: u64 = 500;
55const TEXT_LONG_PRESS_SLOP: f32 = 12.0;
58
59struct LongPressWatcher {
66 controller: TextFieldHandleController,
67 state: TextFieldState,
68 style: TextStyle,
69 start: Point,
70 start_nanos: Cell<Option<u64>>,
71 registration: RefCell<Option<cranpose_core::internal::FrameCallbackRegistration>>,
72 frame_clock: cranpose_core::internal::FrameClock,
73}
74
75impl LongPressWatcher {
76 fn arm(self: &Rc<Self>) {
77 let weak: Weak<LongPressWatcher> = Rc::downgrade(self);
78 let registration = self.frame_clock.with_frame_nanos(move |now| {
79 let Some(watcher) = weak.upgrade() else {
80 return;
81 };
82 watcher.tick(now);
83 });
84 *self.registration.borrow_mut() = Some(registration);
85 crate::request_render_invalidation();
86 }
87
88 fn tick(self: Rc<Self>, now: u64) {
89 self.registration.borrow_mut().take();
90 let Some(press) = self.controller.press() else {
91 return;
92 };
93 let moved = (press.position.x - self.start.x)
94 .abs()
95 .max((press.position.y - self.start.y).abs());
96 if (press.start.x - self.start.x).abs() > 0.5
97 || (press.start.y - self.start.y).abs() > 0.5
98 || moved > TEXT_LONG_PRESS_SLOP
99 {
100 return;
101 }
102 let start = match self.start_nanos.get() {
103 Some(value) => value,
104 None => {
105 self.start_nanos.set(Some(now));
106 now
107 }
108 };
109 if now.saturating_sub(start) < TEXT_LONG_PRESS_MS * 1_000_000 {
110 self.arm();
111 return;
112 }
113 self.controller.claim_gesture();
114 let Some(metrics) = self.controller.metrics() else {
115 return;
116 };
117 let presentation = self.controller.presentation(self.state);
118 let offset = presentation.to_source(window_pos_to_offset(
119 presentation.text(),
120 &self.style,
121 &metrics,
122 self.start,
123 0.0,
124 ));
125 let (word_start, word_end) = self
126 .state
127 .with_text(|text| crate::word_boundaries::find_word_boundaries(text, offset));
128 self.state.edit(|buffer| {
129 buffer.select(TextRange::new(word_start, word_end));
130 });
131 crate::request_render_invalidation();
132 }
133}
134
135fn handle_tip_local_pos(
141 text: &str,
142 style: &TextStyle,
143 metrics: &TextFieldHandleMetrics,
144 offset: usize,
145 affinity: LineAffinity,
146) -> Point {
147 let offset = offset.min(text.len());
148 let (line_index, line_start) = crate::text_field_modifier_node::caret_visual_line_for_offset(
149 text,
150 style,
151 None,
152 metrics.wrap_width,
153 offset,
154 affinity,
155 );
156 let caret_x = measure_text(&AnnotatedString::from(&text[line_start..offset]), style).width;
157 Point {
158 x: metrics.padding_left + caret_x - metrics.scroll_offset,
159 y: metrics.padding_top
160 + line_index as f32 * metrics.line_height
161 + metrics.glyph_box.0
162 + metrics.glyph_box.1,
163 }
164}
165
166fn caret_window_geometry(
167 text: &str,
168 style: &TextStyle,
169 metrics: &TextFieldHandleMetrics,
170 offset: usize,
171 affinity: LineAffinity,
172) -> (Point, Rect) {
173 let tip = handle_tip_local_pos(text, style, metrics, offset, affinity);
174 let bounds = metrics.local_to_window.bounds_for_rect(Rect {
175 x: tip.x,
176 y: tip.y - metrics.glyph_box.1,
177 width: 2.0,
178 height: metrics.glyph_box.1,
179 });
180 (metrics.local_to_window.map_point(tip), bounds)
181}
182
183fn window_pos_to_offset(
190 text: &str,
191 style: &TextStyle,
192 metrics: &TextFieldHandleMetrics,
193 window_pos: Point,
194 y_bias: f32,
195) -> usize {
196 let Some(inverse) = metrics.local_to_window.inverse() else {
197 return 0;
198 };
199 let local = inverse.map_point(Point {
200 y: window_pos.y + y_bias,
201 ..window_pos
202 });
203 let local_x = (local.x - metrics.padding_left + metrics.scroll_offset).max(0.0);
204 let local_y = (local.y - 0.5 * metrics.line_height - metrics.padding_top).max(0.0);
205 crate::text::offset_for_position_wrapped(
206 text,
207 style,
208 None,
209 metrics.wrap_width,
210 metrics.line_height,
211 local_x,
212 local_y,
213 )
214}
215#[composable]
232pub fn BasicTextField(state: TextFieldState, modifier: Modifier, style: TextStyle) -> NodeId {
233 BasicTextFieldWithOptions(
234 state,
235 modifier,
236 BasicTextFieldOptions {
237 text_style: style,
238 ..BasicTextFieldOptions::default()
239 },
240 )
241}
242
243#[derive(Debug, Clone, PartialEq)]
245pub struct BasicTextFieldOptions {
246 pub text_style: TextStyle,
248 pub cursor_color: Color,
250 pub line_limits: TextFieldLineLimits,
252 pub show_keyboard_on_focus: bool,
259 pub keyboard_options: KeyboardOptions,
261 pub on_keyboard_action: Option<KeyboardActionHandler>,
265 pub input_transformation: Option<InputTransformation>,
268 pub output_transformation: Option<OutputTransformation>,
272}
273
274impl Default for BasicTextFieldOptions {
275 fn default() -> Self {
276 Self {
277 text_style: TextStyle::default(),
278 cursor_color: Color(0.0, 0.478, 1.0, 1.0),
279 line_limits: TextFieldLineLimits::default(),
280 show_keyboard_on_focus: true,
281 keyboard_options: KeyboardOptions::default(),
282 on_keyboard_action: None,
283 input_transformation: None,
284 output_transformation: None,
285 }
286 }
287}
288
289#[derive(Clone)]
294pub struct BasicTextFieldDecorationScope {
295 inner: Rc<dyn Fn() -> NodeId>,
296}
297
298impl BasicTextFieldDecorationScope {
299 pub fn inner_text_field(&self) -> NodeId {
300 (self.inner)()
301 }
302}
303
304impl PartialEq for BasicTextFieldDecorationScope {
305 fn eq(&self, other: &Self) -> bool {
306 Rc::ptr_eq(&self.inner, &other.inner)
307 }
308}
309
310#[composable(no_skip)]
319pub fn BasicTextFieldDecorated<D>(
320 state: TextFieldState,
321 modifier: Modifier,
322 options: BasicTextFieldOptions,
323 decoration_box: D,
324) -> NodeId
325where
326 D: Fn(BasicTextFieldDecorationScope) + 'static,
327{
328 let refs = remember(TextFieldRefs::new).with(TextFieldRefs::clone);
329 let decorator = TextFieldDecoratorElement::new(
330 state,
331 refs.clone(),
332 options.text_style.clone(),
333 options.line_limits,
334 crate::modal::local_modal_depth().current(),
335 );
336 let scope = BasicTextFieldDecorationScope {
337 inner: Rc::new(move || {
338 TextFieldNode(
339 state,
340 Modifier::empty(),
341 options.clone(),
342 Some(refs.clone()),
343 false,
344 )
345 }),
346 };
347 Layout(
348 modifier.then(Modifier::from_parts(&[modifier_element(decorator)])),
349 BoxMeasurePolicy::new(Alignment::TOP_START, true),
350 move || decoration_box(scope.clone()),
351 )
352}
353
354#[composable]
358pub fn BasicTextFieldWithOptions(
359 state: TextFieldState,
360 modifier: Modifier,
361 options: BasicTextFieldOptions,
362) -> NodeId {
363 TextFieldNode(state, modifier, options, None, false)
364}
365
366#[composable]
370pub(crate) fn TextFieldNode(
371 state: TextFieldState,
372 modifier: Modifier,
373 options: BasicTextFieldOptions,
374 decorator: Option<TextFieldRefs>,
375 secure: bool,
376) -> NodeId {
377 #[cfg(feature = "localization")]
378 let options = BasicTextFieldOptions {
379 text_style: crate::localization::apply_text_locale(options.text_style),
380 ..options
381 };
382 state.with_text(|_| ());
383 let _selection = state.selection();
384
385 let controller =
386 remember(TextFieldHandleController::new).with(TextFieldHandleController::clone);
387
388 let modal_depth = crate::modal::local_modal_depth().current();
389 let mut text_field_element = TextFieldElement::new(state, options.text_style.clone())
390 .with_cursor_color(options.cursor_color)
391 .with_line_limits(options.line_limits)
392 .with_show_keyboard_on_focus(options.show_keyboard_on_focus)
393 .with_keyboard(options.keyboard_options, options.on_keyboard_action)
394 .with_input_transformation(options.input_transformation)
395 .with_output_transformation(options.output_transformation)
396 .secure(secure)
397 .with_handle_controller(controller.clone())
398 .with_modal_depth(modal_depth);
399 if let Some(refs) = decorator {
400 text_field_element = text_field_element.decorated_by(refs);
401 }
402
403 let text_field_modifier = modifier_element(text_field_element);
404 let final_modifier = Modifier::from_parts(&[text_field_modifier]);
405 let combined_modifier = modifier.then(final_modifier);
406
407 let node = Layout(combined_modifier, EmptyMeasurePolicy, || {});
408
409 BringCaretIntoView(state, options.text_style.clone(), controller.clone());
410
411 SelectionHandles(
412 state,
413 options.text_style,
414 controller,
415 options.cursor_color,
416 secure,
417 );
418
419 node
420}
421
422#[cfg(test)]
423#[path = "tests/basic_text_field_options_tests.rs"]
424mod options_tests;
425
426fn caret_window_rect(
430 text: &str,
431 style: &TextStyle,
432 metrics: &TextFieldHandleMetrics,
433 offset: usize,
434) -> Rect {
435 caret_window_geometry(text, style, metrics, offset, LineAffinity::Upstream).1
436}
437
438#[composable]
448fn BringCaretIntoView(
449 state: TextFieldState,
450 style: TextStyle,
451 controller: TextFieldHandleController,
452) {
453 let Some(metrics) = controller.metrics() else {
454 return;
455 };
456 let ime_bottom = local_ime_insets().current().bottom;
457 let responder = local_bring_into_view_responder().current();
458
459 let previous: Rc<Cell<Option<(usize, usize, i64)>>> =
460 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
461
462 if !metrics.focused {
463 previous.set(None);
464 return;
465 }
466 let Some(responder) = responder else {
467 return;
468 };
469
470 let presentation = controller.presentation(state);
471 let selection = state.selection();
472
473 let key = (
474 selection.start,
475 selection.end,
476 (ime_bottom * 4.0).round() as i64,
477 );
478 SideEffect(move || {
479 if previous.get() == Some(key) {
480 return;
481 }
482 previous.set(Some(key));
483 let Some(metrics) = controller.metrics_now() else {
484 return;
485 };
486 let caret = caret_window_rect(
487 presentation.text(),
488 &style,
489 &metrics,
490 presentation.to_shown(selection.start),
491 );
492 responder.bring_into_view(caret, ime_bottom);
493 });
494}
495
496#[composable]
501fn SelectionHandles(
502 state: TextFieldState,
503 style: TextStyle,
504 controller: TextFieldHandleController,
505 accent: Color,
506 secure: bool,
507) {
508 let selection = state.selection();
509 let current_range = (selection.min(), selection.max());
510 let active_press = controller.press();
511
512 let menu_open = remember(|| mutableStateOf(true)).with(|state| *state);
513 let caret_menu_open = remember(|| mutableStateOf(false)).with(|state| *state);
514 let caret_menu_offset: Rc<Cell<usize>> =
515 remember(|| Rc::new(Cell::new(0usize))).with(Rc::clone);
516 let previous_range: Rc<Cell<(usize, usize)>> =
517 remember(|| Rc::new(Cell::new(current_range))).with(Rc::clone);
518 {
519 let previous_range = Rc::clone(&previous_range);
520 SideEffect(move || {
521 if previous_range.get() != current_range {
522 previous_range.set(current_range);
523 menu_open.set(true);
524 }
525 });
526 }
527 {
528 let caret_menu_offset = Rc::clone(&caret_menu_offset);
529 let caret_start = selection.start;
530 SideEffect(move || {
531 if caret_menu_open.value()
532 && (!selection.collapsed() || caret_start != caret_menu_offset.get())
533 {
534 caret_menu_open.set(false);
535 }
536 });
537 }
538
539 let Some(metrics) = controller.metrics() else {
540 return;
541 };
542 if !metrics.focused || !metrics.direct_manipulation {
543 return;
544 }
545 let Some(metrics) = controller.live_metrics() else {
548 return;
549 };
550
551 let presentation = controller.presentation(state);
552 let text = presentation.text();
553
554 let press_watcher: Rc<Cell<Option<(u32, u32)>>> =
555 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
556 let press_watcher_ref: Rc<RefCell<Option<Rc<LongPressWatcher>>>> =
557 remember(|| Rc::new(RefCell::new(None))).with(Rc::clone);
558 match active_press {
559 Some(press) => {
560 let key = (press.start.x.to_bits(), press.start.y.to_bits());
561 if press_watcher.get() != Some(key) {
562 press_watcher.set(Some(key));
563 let watcher = Rc::new(LongPressWatcher {
564 controller: controller.clone(),
565 state,
566 style: style.clone(),
567 start: press.start,
568 start_nanos: Cell::new(None),
569 registration: RefCell::new(None),
570 frame_clock: cranpose_core::with_current_composer(|composer| {
571 composer.runtime_handle()
572 })
573 .frame_clock(),
574 });
575 watcher.arm();
576 *press_watcher_ref.borrow_mut() = Some(watcher);
577 }
578 }
579 None => {
580 press_watcher.set(None);
581 press_watcher_ref.borrow_mut().take();
582 }
583 }
584
585 let drag_pos: MutableState<Option<Point>> =
586 remember(|| mutableStateOf(None::<Point>)).with(|state| *state);
587 let drag_bias: Rc<Cell<Option<HandleGrabOffset>>> =
588 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
589 let last_dragged: Rc<Cell<Option<HandleKind>>> =
590 remember(|| Rc::new(Cell::new(None))).with(Rc::clone);
591 let menu_anchor_range: Rc<Cell<(usize, usize)>> =
592 remember(|| Rc::new(Cell::new(current_range))).with(Rc::clone);
593 {
594 let last_dragged = Rc::clone(&last_dragged);
595 let menu_anchor_range = Rc::clone(&menu_anchor_range);
596 SideEffect(move || {
597 if menu_anchor_range.get() != current_range {
598 menu_anchor_range.set(current_range);
599 if drag_pos.value().is_none() {
600 last_dragged.set(None);
601 }
602 }
603 });
604 }
605 let cursor_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
606 let start_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
607 let end_tip_y: Rc<Cell<f32>> = remember(|| Rc::new(Cell::new(0.0f32))).with(Rc::clone);
608
609 if selection.collapsed() {
610 let (tip, caret_rect) = caret_window_geometry(
611 text,
612 &style,
613 &metrics,
614 presentation.to_shown(selection.start),
615 LineAffinity::Upstream,
616 );
617 let on_drag = drag_caret_closure(state, style.clone(), controller, Rc::clone(&drag_bias));
618 let open_caret_menu = {
619 let caret_menu_offset = Rc::clone(&caret_menu_offset);
620 move || {
621 caret_menu_offset.set(state.selection().start);
622 caret_menu_open.set(true);
623 }
624 };
625 let on_tap = open_caret_menu.clone();
626 let on_long_press = open_caret_menu;
627 let grab_bias = Rc::clone(&drag_bias);
628 let end_bias = Rc::clone(&drag_bias);
629 cursor_tip_y.set(tip.y);
630 let tip_y = Rc::clone(&cursor_tip_y);
631 SelectionHandle(
632 HandleKind::Cursor,
633 tip,
634 caret_rect.height,
635 HANDLE_RADIUS,
636 accent,
637 move |pos| {
638 track_handle_grab(&grab_bias, HandleKind::Cursor, tip_y.get(), pos.y);
639 drag_pos.set(Some(pos));
640 on_drag(pos);
641 },
642 move || {
643 drag_pos.set(None);
644 end_bias.set(None);
645 crate::cursor_animation::reset_cursor_blink();
646 },
647 on_long_press,
648 on_tap,
649 );
650
651 if caret_menu_open.value() {
652 let can_paste = clipboard_can_paste();
653 let can_undo = state.can_undo();
654 let can_redo = state.can_redo();
655 let undo_state = state;
656 let redo_state = state;
657 CaretActionMenu(
658 MenuAnchor {
659 center_x: tip.x,
660 line_top: caret_rect.y,
661 line_bottom: caret_rect.y + caret_rect.height,
662 },
663 drag_pos.value().is_none(),
664 can_paste,
665 can_undo,
666 can_redo,
667 move || {
668 clipboard_paste_into_focus();
669 caret_menu_open.set(false);
670 },
671 move || {
672 dispatch_select_all();
673 caret_menu_open.set(false);
674 },
675 move || {
676 undo_state.undo();
677 crate::request_render_invalidation();
678 caret_menu_open.set(false);
679 },
680 move || {
681 redo_state.redo();
682 crate::request_render_invalidation();
683 caret_menu_open.set(false);
684 },
685 );
686 }
687 } else {
688 let (start, end) = presentation.range_to_shown(selection);
689 let (start_tip, start_rect) =
690 caret_window_geometry(text, &style, &metrics, start, LineAffinity::Downstream);
691 let (end_tip, end_rect) =
692 caret_window_geometry(text, &style, &metrics, end, LineAffinity::Upstream);
693
694 let last_dragged_start = Rc::clone(&last_dragged);
695 let last_dragged_end = Rc::clone(&last_dragged);
696 let on_drag_start = drag_edge_closure(
697 HandleKind::SelectionStart,
698 state,
699 style.clone(),
700 controller.clone(),
701 Rc::clone(&drag_bias),
702 );
703 let grab_bias = Rc::clone(&drag_bias);
704 let end_bias = Rc::clone(&drag_bias);
705 start_tip_y.set(start_tip.y);
706 let start_tip_live = Rc::clone(&start_tip_y);
707 SelectionHandle(
708 HandleKind::SelectionStart,
709 start_tip,
710 start_rect.height,
711 HANDLE_RADIUS,
712 accent,
713 move |pos| {
714 track_handle_grab(
715 &grab_bias,
716 HandleKind::SelectionStart,
717 start_tip_live.get(),
718 pos.y,
719 );
720 last_dragged_start.set(Some(HandleKind::SelectionStart));
721 drag_pos.set(Some(pos));
722 on_drag_start(pos);
723 },
724 move || {
725 drag_pos.set(None);
726 end_bias.set(None);
727 },
728 move || menu_open.set(true),
729 move || menu_open.set(true),
730 );
731
732 let on_drag_end = drag_edge_closure(
733 HandleKind::SelectionEnd,
734 state,
735 style.clone(),
736 controller.clone(),
737 Rc::clone(&drag_bias),
738 );
739 let grab_bias = Rc::clone(&drag_bias);
740 let end_bias = Rc::clone(&drag_bias);
741 end_tip_y.set(end_tip.y);
742 let end_tip_live = Rc::clone(&end_tip_y);
743 SelectionHandle(
744 HandleKind::SelectionEnd,
745 end_tip,
746 end_rect.height,
747 HANDLE_RADIUS,
748 accent,
749 move |pos| {
750 track_handle_grab(
751 &grab_bias,
752 HandleKind::SelectionEnd,
753 end_tip_live.get(),
754 pos.y,
755 );
756 last_dragged_end.set(Some(HandleKind::SelectionEnd));
757 drag_pos.set(Some(pos));
758 on_drag_end(pos);
759 },
760 move || {
761 drag_pos.set(None);
762 end_bias.set(None);
763 },
764 move || menu_open.set(true),
765 move || menu_open.set(true),
766 );
767
768 if menu_open.value() {
769 let can_paste = clipboard_can_paste();
770 let slide_point = if controller.gesture_claimed() {
771 active_press.map(|press| press.position)
772 } else {
773 None
774 };
775 let (center_x, caret_rect) = match last_dragged.get() {
776 Some(HandleKind::SelectionStart) => (start_tip.x, start_rect),
777 Some(HandleKind::SelectionEnd | HandleKind::Cursor) => (end_tip.x, end_rect),
778 None => ((start_tip.x + end_tip.x) * 0.5, start_rect),
779 };
780 TextSelectionMenu(
781 MenuAnchor {
782 center_x,
783 line_top: caret_rect.y,
784 line_bottom: caret_rect.y + caret_rect.height,
785 },
786 drag_pos.value().is_none(),
787 slide_point,
788 !secure,
789 can_paste,
790 move || {
791 if let Some(text) = dispatch_copy() {
792 clipboard_write_text(&text);
793 }
794 menu_open.set(false);
795 },
796 move || {
797 if let Some(text) = dispatch_cut() {
798 clipboard_write_text(&text);
799 }
800 menu_open.set(false);
801 },
802 move || {
803 clipboard_paste_into_focus();
804 menu_open.set(false);
805 },
806 move || {
807 dispatch_select_all();
808 menu_open.set(false);
809 },
810 );
811 }
812 }
813
814 let loupe_target = drag_pos.value().and_then(|finger| {
815 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
816 let offset = window_pos_to_offset(text, &style, &metrics, finger, bias);
817 let (_, caret_rect) =
818 caret_window_geometry(text, &style, &metrics, offset, LineAffinity::Upstream);
819 loupe_target_for_drag(finger, caret_rect.y + caret_rect.height, caret_rect.height)
820 });
821 SelectionLoupe(loupe_target);
822}
823
824fn track_handle_grab(
825 drag_bias: &Cell<Option<HandleGrabOffset>>,
826 kind: HandleKind,
827 handle_tip_y: f32,
828 finger_y: f32,
829) -> f32 {
830 let drifts = kind != HandleKind::SelectionStart;
831 let mut grab = drag_bias
832 .get()
833 .unwrap_or_else(|| HandleGrabOffset::begin_for(handle_tip_y, finger_y, drifts));
834 let bias = grab.track(finger_y);
835 drag_bias.set(Some(grab));
836 bias
837}
838
839fn drag_caret_closure(
843 state: TextFieldState,
844 style: TextStyle,
845 controller: TextFieldHandleController,
846 drag_bias: Rc<Cell<Option<HandleGrabOffset>>>,
847) -> Rc<dyn Fn(Point)> {
848 Rc::new(move |window_pos: Point| {
849 let Some(metrics) = controller.metrics() else {
850 return;
851 };
852 let presentation = controller.presentation(state);
853 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
854 let offset = presentation.to_source(window_pos_to_offset(
855 presentation.text(),
856 &style,
857 &metrics,
858 window_pos,
859 bias,
860 ));
861 state.set_selection(TextRange::new(offset, offset));
862 crate::cursor_animation::suspend_cursor_blink();
863 crate::request_render_invalidation();
864 })
865}
866
867fn drag_edge_closure(
871 dragged: HandleKind,
872 state: TextFieldState,
873 style: TextStyle,
874 controller: TextFieldHandleController,
875 drag_bias: Rc<Cell<Option<HandleGrabOffset>>>,
876) -> Rc<dyn Fn(Point)> {
877 Rc::new(move |window_pos: Point| {
878 let Some(metrics) = controller.metrics() else {
879 return;
880 };
881 let presentation = controller.presentation(state);
882 let bias = drag_bias.get().map_or(0.0, |grab| grab.bias());
883 let dragged_offset = presentation.to_source(window_pos_to_offset(
884 presentation.text(),
885 &style,
886 &metrics,
887 window_pos,
888 bias,
889 ));
890 let selection = state.selection();
891 let fixed_edge = match dragged {
892 HandleKind::SelectionStart => selection.max(),
893 _ => selection.min(),
894 };
895 let (min, max) = selection_after_handle_drag(
896 dragged,
897 fixed_edge,
898 dragged_offset,
899 state.with_text(str::len),
900 );
901 state.set_selection(TextRange::new(min, max));
902 crate::request_render_invalidation();
903 })
904}
905
906#[cfg(test)]
907#[path = "tests/basic_text_field_tests.rs"]
908mod tests;